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.
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:
Suppose you have 2 complicated classes, ReactiveCustomer
and ReactiveBartender
injected into your MainWindowViewModel
.
// 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. 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. 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.
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:
Age
setterMainWindowViewModel
receives the eventAnActiveBartender
depending on whether the customer's new age is above 18AnActiveBartender
's GreetingMessage
setter<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>
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.