Search code examples
design-patternssoftware-designreactiveuiavalonia

Why use "WhenAnyValue" instead of "RaisePropertyChanged"?


I've been reading up on Avalonia and this example shows adding a property and then in the viewmodel constructor use "WhenAnyValue" to raise the "RaisePropertyChanged" event.

What is the point?

Why not just use "RaisePropertyChanged" in the setter of the property? What benefits does the "WhenAnyValue" method have, since raising the event in the setter is much cleaner.


Solution

  • Purpose of WhenAnyValue

    For tiny projects where everything slightly detached from business logic is written in the MainWindowViewModel, the use case of this method does seem perplexing. But as the project scales, it becomes necessary to inject dependency into the MainWindowViewModel instead of cramming most stuff into a single class.

    So if your project is utilizing dependency injection, you will need this WhenAnyValue method observe changes of properties in other classes injected into MainWindowViewModel, and notify other injected stuff to react to the change. Here's an example:

    Example

    Suppose you have 2 complicated classes, ReactiveCustomer and ReactiveBartender injected into your MainWindowViewModel.

    MainWindowViewModel Class Definition

    // MainWindowViewModel class definition
    // Contains an instance of ReactiveCustomer and ReactiveBartender as public fields
    using System;
    using ReactiveUI;
    
    namespace WhyUseWhenAnyValue.ViewModels;
    
    public class MainWindowViewModel : ReactiveObject
    {
        public MainWindowViewModel()
        {
            AnReactiveCustomer = new ReactiveCustomer("Rich Ash", 15);
            AnReactiveBartender = new ReactiveBartender();
    
            // create an observable by WhenAnyValue that observes the field Age of AnReactiveCustomer
            // whenever AnReactiveCustomer's age changes, run the observer in Subscribe
            this.WhenAnyValue(x => x.AnReactiveCustomer.Age).Subscribe(
                (int newCustomerAge) => { 
                    AnReactiveBartender.GreetingMessage = newCustomerAge ? $"Hello {AnReactiveCustomer.Name}, here is the menu for cocktail." : $"Hello {AnReactiveCustomer.Name}, you can only have mocktails and juice";
                }
            );
        }
    
        public ReactiveCustomer  AnReactiveCustomer { get; }
        public ReactiveBartender AnReactiveBartender { get; }
    
    }
    
    

    How AnReactiveBartender greets the customer depends on whether the age of AnReactiveCustomer is above 18. Here are the definitions of these two classes. Please note that both of them also inherits ReactiveObject.

    ReactiveBartender Class definition

    // ReactiveBartender class definition. Please note that it also inherits ReactiveObject
    // This class is a member of ViewModel namespace
    
    namespace WhyUseWhenAnyValue.ViewModels;
    
    public class ReactiveBartender : ReactiveObject
    {
        public ReactiveBartender()
        {
            _greetingMessage = "Default greeting message";
        }
        public string GreetingMessage 
        {
            get => _greetingMessage;
            set => this.RaiseAndSetIfChanged(ref _greetingMessage, value);
        }
        private string _greetingMessage;
    }
    

    ReactiveCustomer Class Definition

    // ReactiveCustomer class definition. Please note that it also inherits ReactiveObject
    // This class is a member of ViewModel namespace which 
    // wraps around another class Customer defined in Models namespace
    
    using System.Reactive;
    using ReactiveUI;
    using WhyUseWhenAnyValue.Models;
    
    namespace WhyUseWhenAnyValue.ViewModels;
    
    
    public class ReactiveCustomer : ReactiveObject
    {
        public ReactiveCustomer(string name, int age)
        {
            _customer = new Customer(name, age);
            GrowUpCommand = ReactiveCommand.Create(_reactiveGrowUp);
        }
        public int Age
        {
            get => _customer.Age;
            set => this.RaiseAndSetIfChanged(ref _customer.Age, value);
        }
        public string Name
        {
            get => _customer.Name;
        }
        public ReactiveCommand<Unit, Unit> GrowUpCommand { get; }
        private void _reactiveGrowUp()
        {
            _customer.GrowUp();
            this.RaisePropertyChanged(nameof(_customer.Age));
        }
        // class Customer is defined in the Models namespace, containing tons of business logic
        private Customer _customer;
    }
    

    The ReactiveCustomer exposes a reactive command GrowUpCommand that increments its age by 1. Everytime the customer's age is incremented, a PropertyChanged event is raised since there is a RaiseAndSetIfChanged in the setter of the Age field.

    Why RaisePropertyChanged Isn't Suitable

    The bartender's greeting message is dependent on whether the customer is above drinking age. But he/she cannot receive the PropertyChanged event raised in the ReactiveCustomer class since ReactiveBartender contains no mechanism observing ReactiveCustomer. After GrowUpCommand is run 100 times and the customer is at age 115, the bartender still thinks he/she is below drinking age. Unless we explictly notifies the bartender of change from MainWindowViewModel, where both the customer and bartender live.

    Take a look at the constructor of MainWindowViewModel, there is an observable created by WhenAnyValue that observes the Age property of AnReactiveCustomer. When the age of AnReactiveCustomer is changed, a PropertyChanged event is raised by the setter of Age. The observable receives this event and runs what's defined in the Subscribe method. In the Subscribe method lives what's called an IObserver, which is a lambda defining what to do upon receiving the PropertyChanged event. In this case, the observer checks if the age of AnReactiveCustomer is above 18, and set the GreetingMessage of AnReactiveBartender accordingly.

    In summary, here's the series of actions that take place upon the Age of AnReactiveCustomer changes:

    1. PropertyChanged event raised in the Age setter
    2. IObservable in the MainWindowViewModel receives the event
    3. IObserver sets the greeting message of the AnActiveBartender depending on whether the customer's new age is above 18
    4. PropertyChanged event raised in the AnActiveBartender's GreetingMessage setter
    5. The UI updates the greeting message textbox bound to the bartender's greeting message.

    View Definition

    <Window xmlns="https://github.com/avaloniaui"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:vm="using:WhyUseWhenAnyValue.ViewModels"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
            x:Class="WhyUseWhenAnyValue.Views.MainWindow"
            x:DataType="vm:MainWindowViewModel"
            Icon="/Assets/avalonia-logo.ico"
            Title="WhyUseWhenAnyValue">
    
        <Design.DataContext>
            <!-- This only sets the DataContext for the previewer in an IDE,
                 to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
            <vm:MainWindowViewModel/>
        </Design.DataContext>
        <StackPanel>
            <TextBlock Text="{Binding AnReactiveBartender.GreetingMessage}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <Button Content="GrowUpBro" Command="{Binding AnReactiveCustomer.GrowUpCommand}"/>
        </StackPanel>
    
    </Window>
    

    Conclusion

    So to simply answer your question "Why use WhenAnyValue instead of RaisePropertyChanged?", it's because in case that your MainWindowViewModel has dependencies injected into it, it is necessary to create another observable using WhenAnyValue that observe the properties of one class instance, and notify the other class instance of change. WhenAnyValue is not meant to replace RaisePropertyChanged, they both have their use cases.