Diego Rafael Souza
Diego Rafael Souza

Reputation: 5313

Child properties update calling it's parent's `OnPropertyChanged`

I'm trying to create a XF component whose some properties are of a type that inherits from BindableObject. For illustrating, I have class Shadow with double Radius and Color ShadowColor properties and a class MyBoxText, that have a bool IsLoading and a Shadow Ghost properties.

My View and it's Bindings is working as expected, but I have an issue with it's custom renderer:

When I change the Ghost properties I need to redraw the entire view (of the MyBoxText control), to visually update the shadow color, for example.

Here's some mcve:

Classes code:

public class MyBoxText : Label /* It's bindable by inheritance */
{
    #region Properties
    public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(MyBoxText), false) ;
    public bool IsLoading
    {
        get { return (bool)GetValue(IsLoadingProperty); }
        set { SetValue(IsLoadingProperty, value); }
    }

    public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null) ;
    public Shadow Ghost
    {
        get { return (Shadow)GetValue(GhostProperty); }
        set { SetValue(GhostProperty, value); }
    }
    #endregion
}

public class Shadow : BindableObject /* It's explictly bindable */
{
    #region Properties
    public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(Shadow), Color.Black) ;
    public Color ShadowColor
    {
        get { return (Color)GetValue(ShadowColorProperty); }
        set { SetValue(ShadowColorProperty, value); }
    }

    public static readonly BindableProperty ShadowRadiusProperty = BindableProperty.Create(nameof(ShadowRadius), typeof(double), typeof(Shadow), 20) ;
    public double ShadowRadius
    {
        get { return (double)GetValue(ShadowRadiusProperty); }
        set { SetValue(ShadowRadiusProperty, value); }
    }
    #endregion

    public Shadow()
    {

    }
}

My renderer's code is like this:

public class MyBoxText : LabelRenderer
{
    public MyBoxText()
    {
        SetWillNotDraw(false);
    }

    public override void Draw(Canvas canvas)
    {
        MyBoxText myView = (MyBoxText)this.Element;

        // Some drawing logic
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (e.PropertyName == MyBoxText.IsLoadingProperty.PropertyName  ||
            e.PropertyName == MyBoxText.GhostProperty.PropertyName )
            Invalidate();
    }
}

The issue is that when I change the Ghost.ShadowColor property my 'OnElementPropertyChanged' override is not called, and the View stays with the old color on the screen.

Is there a way to propagate the child's 'Property Update' event to parent view 'Property Changed' or another way to achieve this?

Upvotes: 1

Views: 1700

Answers (2)

Diego Rafael Souza
Diego Rafael Souza

Reputation: 5313

Thanks to Elvis's answer I got it. Based on his idea I've made some changing to reuse it on other components and I'm sharing it now just in case someone else needs something like this.

I thought that use it this way we could get a cleaner and simple code:

public class MyBoxText : Label /* It's bindable by inheritance */
{
    // Added this as private property
    private ChangingPropagator changingPropagator;
    private ChangingPropagator ChangingPropagator
    {
        get
        {
            if (changingPropagator == null)
                changingPropagator = new ChangingPropagator(this, OnPropertyChanged, nameof(Shadow.ShadowColor), nameof(Shadow.ShadowRadius));

            return changingPropagator;
        }
    }

    #region Properties
    public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(MyBoxText), false) ;
    public bool IsLoading
    {
        get { return (bool)GetValue(IsLoadingProperty); }
        set { SetValue(IsLoadingProperty, value); }
    }

    public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null) ;
    public Shadow Ghost
    {
        // Here I use the ChangingPropagator's Getter and Setter instead of the deafult ones:
        get { return ChangingPropagator.GetValue<Shadow>(GhostProperty); }
        set { ChangingPropagator.SetValue(GhostProperty,ref value); }
    }
    #endregion
}

And it's the ChangingPropagator class:

public class ChangingPropagator
{
    string[] listenedProperties = new string[0];
    Action<string> changesNotifyer = null;
    BindableObject propagationRootObject = null;
    List<KeyValuePair<string, object>> propagationProperties = new List<KeyValuePair<string, object>>();

    public ChangingPropagator(BindableObject bindableObject, Action<string> onPropertyChangedMethod, params string[] propertyToListenTo)
    {
        changesNotifyer = onPropertyChangedMethod;
        propagationRootObject = bindableObject;
        listenedProperties = propertyToListenTo ?? listenedProperties;

        // ToDo: Add some consistency checks
    }

    public void AddPropertyToListenTo(params string[] propertyName)
    {
        listenedProperties = listenedProperties.Union(propertyName).ToArray();
    }

    // I need handle it here too 'cause when I use the child `Ghost` property coming from XAML binding, it didn't hit the `set` method
    public T GetValue<T>(BindableProperty property)
    {
        var value = propagationRootObject?.GetValue(property);

        if (value != null)
        {
            INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);

            if (bindableSubObject != null)
            {
                bindableSubObject.PropertyChanged -= PropagatorListener;
                bindableSubObject.PropertyChanged += PropagatorListener;

                if (!propagationProperties.Any(a => a.Key == property.PropertyName))
                    propagationProperties.Add(new KeyValuePair<string, object>(property.PropertyName, value));
            }
        }

        return (T)value;
    }

    public void SetValue<T>(BindableProperty property, ref T value)
    {
        var oldValue = propagationRootObject?.GetValue(property);

        if (oldValue != null)
        {
            INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);

            if (bindableSubObject != null)
                bindableSubObject.PropertyChanged -= PropagatorListener;
        }

        if (value != null)
        {
            INotifyPropertyChanged bindableSubObject = (value as INotifyPropertyChanged);
            if (bindableSubObject != null)
            {
                bindableSubObject.PropertyChanged += PropagatorListener;

                propagationProperties.RemoveAll(p => p.Key == property.PropertyName);
                propagationProperties.Add(new KeyValuePair<string, object>(property.PropertyName, value));
            }
        }

        propagationRootObject.SetValue(property, value);
    }

    private void PropagatorListener(object sender, PropertyChangedEventArgs e)
    {
        if (listenedProperties?.Contains(e.PropertyName) ?? true)
            PropagationThrower(sender);
    }

    private void PropagationThrower(object sender)
    {
        if (propagationProperties.Any(p => p.Value == sender))
        {
            var prop = propagationProperties.FirstOrDefault(p => p.Value == sender);
            changesNotifyer?.Invoke(prop.Key);
        }
    }
}

Upvotes: 1

Elvis Xia - MSFT
Elvis Xia - MSFT

Reputation: 10831

The issue is that when I change the Ghost.ShadowColor property my 'OnElementPropertyChanged' override is not called, and the View stays with the old color on the screen. Is there a way to propagate the child's 'Property Update' event to parent view 'Property Changed' or another way to achieve this?

Yes, there is a way. Since your Shadow inherits from BindableObject, which implements the INotifyPropertyChanged Interface. You can set notify ShadowColor change:

  1. Add OnPropertyChanged() to Setter of ShadowColor in Shadow.cs:

    public class Shadow : BindableObject /* It's explictly bindable */
    {
        #region Properties
        public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(Shadow), Color.Black);
        public Color ShadowColor
        {
            get { return (Color)GetValue(ShadowColorProperty); }
            set { SetValue(ShadowColorProperty, value);
                //Notify the ShadowColorProperty Changed
                OnPropertyChanged();
            }
        }
       ...
    }
    
  2. Modify your MyBoxText.cs like this:

    public class MyBoxText : Label /* It's bindable by inheritance */
    {
     ...
    
        public static readonly BindableProperty GhostProperty = BindableProperty.Create(nameof(Ghost), typeof(Shadow), typeof(MyBoxText), null);
        public Shadow Ghost
        {
            get { return (Shadow)GetValue(GhostProperty); }
            set {
                //register the ShadowColor change event
                value.PropertyChanged += ShadowColor_PropertyChanged;
                SetValue(GhostProperty, value); }
        }
    
        private void ShadowColor_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            //unregister the event
            this.Ghost.PropertyChanged -= ShadowColor_PropertyChanged;
            //set this.Ghost to a new object with new ShadowColor to trigger the OnPropertyChanged
            this.Ghost = new Shadow
            {
                ShadowColor = (sender as Shadow).ShadowColor,
                ShadowRadius = Ghost.ShadowRadius
            };
        }
    }
    

Upvotes: 2

Related Questions