xamarinmvvmmauiobservableobjectxamarin-community-toolkit

How to use ObservableObject for interlinked properties?


I am trying to convert this working view model to one using ObservableObject from Community Toolkit to reduce boilerplate.

public class HslVM : INotifyPropertyChanged
{
    private float hue, saturation, luminosity;
    private Color color = default!;

    public float Hue
    {
        get => hue;
        set
        {
            if (hue != value)
                Color = Color.FromHsla(value, saturation, luminosity);
        }
    }


    public float Saturation
    {
        get => saturation;
        set
        {
            if (saturation != value)
                Color = Color.FromHsla(hue, value, luminosity);
        }
    }

    public float Luminosity
    {
        get => luminosity;
        set
        {
            if (luminosity != value)
                Color = Color.FromHsla(hue, saturation, value);
        }
    }

    public Color Color
    {
        get => color;
        set
        {
            if (color == value) return;

            color = value;
            OnPropertyChanged();


            if (hue != color.GetHue())
            {
                hue = color.GetHue();
                OnPropertyChanged(nameof(Hue));
            }

            if (saturation != color.GetSaturation())
            {
                saturation = color.GetSaturation();
                OnPropertyChanged(nameof(Saturation));
            }

            if (luminosity != color.GetLuminosity())
            {
                luminosity = color.GetLuminosity();
                OnPropertyChanged(nameof(Luminosity));
            }
        }
    }


    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string name = "")
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

My attempt below does not work.

public partial class HslVM : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Color))]
    float hue;


    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Color))]
    float saturation;


    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Color))]
    float luminosity;


    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Hue))]
    [NotifyPropertyChangedFor(nameof(Saturation))]
    [NotifyPropertyChangedFor(nameof(Luminosity))]
    Color color = default!;
}

Solution

  • Lets look at ObservableProperty attribute doc, and write the equivalent of your new code:

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Color))]
    float hue;
    
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Hue))]
    [NotifyPropertyChangedFor(nameof(Saturation))]
    [NotifyPropertyChangedFor(nameof(Luminosity))]
    Color color = default!;
    

    generates properties something like this:

    public float Hue
    {
      get => hue;
      set {
        if (SetProperty(ref hue, value))
          OnPropertyChanged(nameof(Color);
      }
    }
    
    public Color Color
    {
      get => color;
      set {
        if (SetProperty(ref color, value))
        {
          OnPropertyChanged(nameof(Hue));
          OnPropertyChanged(nameof(Saturation));
          OnPropertyChanged(nameof(Value));
        }
      }
    }
    

    Compare that to your original working version. Its missing all the "smarts" in the setters, that set the correct values of other fields.

    All ObservableProperty does is generate that simple "default" property implementation. That isn't useful in this situation. AFAIK, it lacks any way to "inject" the needed smarts.

    You might be tempted to keep Color setter from original, and change the others to ObservableProperty. That won't work either. OnPropertyChanged(nameof(Color)), or the equivalent attribute NotifyPropertyChangedFor(nameof(Color)) cannot tell Color HOW to update; it can only tell anyone listening that Color has changed. That won't correct the color.