I’ve been recently working with Avalonia to serve my .NET UI, and since the documentation is still somewhat lacking on this fledgling UI framework, I thought I’d work out and then explain the two-way reactivity that AvaloniaUI operates in conjunction with ReactiveUI.
So, let’s assume we’re working with an Avalonia MVVM solution, with a very basic MainWindowViewModel
, and a MainWindow.xaml
Reacting to Interactivity: Command Binding
Starting fresh; let’s add a simple button to the MainWindow.xaml
:
<Button Command="{Binding ButtonClicked}">Menu</Button>
As you can see, I’ve added the Command
attribute, with a Binding
called ButtonClicked
A Binding is, put simply, a signal to the ViewModel that you want this attribute (in this case the Command
to be attached to a process or property. You’re telling the ViewModel that something’s supposed to happen. In our case above, we’re indicated that we want the Command attribute of the Button to be bound to something called ButtonClicked
. What is ButtonClicked
? It’s a property I’ve defined in the ViewModel
, I’ll show you:
using ReactiveUI; ... public ReactiveCommand<Unit, Unit> ButtonClicked { get; }
ButtonClicked is a new property, which is an instance of ReactiveUI’s ReactiveCommand
.
Because the XAML file declares that object instance denoted by the name ButtonClicked
as being it’s Binding
, then whenever the Button
is clicked, the ButtonClicked
instance will be fired.
The next part is how do we register a method to this instance, so that we can actually do something tangible in reaction to the button click.
Let’s create a very simple method called Clicked
:
void Clicked() { Console.WriteLine("Clicked"); }
We need to make sure that our ButtonClicked
instance will invoke this method, so let’s register them in the Constructor:
public MainWindowViewModel() { ButtonClicked = ReactiveCommand.Create(Clicked); }
So, upon creation of the ViewModel
, the first thing that’ll happen is that we’ll create a new ButtonClicked
instance specifically to invoke the Clicked
method. That ButtonClicked
instance is then bound to the Button
‘s Command
.
When the Command
is invoked (by a click), it will check to see which method is registered with it, and call it. In this case, that’ll result in a Console Line.
Here is the basic flow of this system:
- Window is opened
ButtonClicked
ReactiveCommand
property is attached to theClicked
methodButton
is clicked by the userBinding
property (ButtonClicked
) is accessed- Attached method (
Clicked
) is called
Reacting To Change: Property Binding
For this, I’m going to add a text block, and we’ll change the text within the block by the button click. We’ve done the button click already, now we just need to ‘react’ to the change in string value; here’s the XAML for the TextBlock:
<TextBlock Text="{Binding GettingStarted}" TextAlignment="Center"/>
Similar to the button, I’m Binding the TextBlock to a Property. In this case, I’m Binding the TextBlock’s Text attribute to a property called GettingStarted. We’ll start with a naive and broken implementation and then explain how Avalonia comes in and fixes it up:
public MainWindowViewModel() { GettingStarted = "To get started, click on the menu!"; ButtonClicked = ReactiveCommand.Create(ChangeGreetingText); } public ReactiveCommand<Unit, Unit> ButtonClicked { get; } public string GettingStarted { get; set; } void ChangeGreetingText() { GettingStarted = "Great, you can follow instructions!"; }
So, this looks logical; I’ve bound my TextBlock to the GettingStarted property, and I’ve a system in place to set and change the text held within it. Unfortunately, changing the GettingStarted
property will not change the TextBlock
, despite having Two-Way binding. The key reason for this is that I’m not telling the UI that something’s changed. Let’s fix it with Avalonia.
First off, we need to adjust the class that the ViewModel extends from, as the ViewModelBase
doesn’t know how to fire the correct events to talk to the UI in the way that we want:
using Avalonia; ... public class MainWindowViewModel : AvaloniaObject
The AvaloniaObject
provides us some useful methods to get and set properties which then internally tell the UI that something’s changed. In order to manage this, I need to declare a Reactive AvaloniaProperty
:
public static readonly AvaloniaProperty GettingStartedReactive = AvaloniaProperty.Register<MainWindowViewModel, string>("GettingStarted");
The AvaloniaProperty needs to be static, and it needs to provide adequate instruction as to what non-static property it’s attached to. The best explanation I can give is that this command is registering a property named ‘GettingStarted’, as a string, to the MainWindowViewModel
. I’ve called this property GettingStartedReactive
just to separate it logically from the property it oversees GettingStarted.
Now, in order to make this truly reactive, I need to adjust the GettingStarted
property to ping the GettingStartedReactive
property every time it changes.
public string GettingStarted { get => this.GetValue(GettingStartedReactive); set => this.SetValue(GettingStartedReactive, value); }
GetValue
and SetValue
are methods found as part of the AvalonObject
, and passing in our Reactive AvalonProperty
is the final piece to this puzzle. Here is the final flow:
- Window is opened
ButtonClicked
ReactiveCommand
property is attached to theChangeGreetingText
methodButton
is clicked by the userBinding
property (ButtonClicked
) is accessed- Attached method (
ChangeGreetingText
) is called - String Value of GettingStarted is changed
- GettingStarted Setter is called
AvalonObject
‘sSetValue
method is called, passing in anAvalonProperty
initialised with ourGettingStarted
property- The
AvalonProperty
handles the changing of the string value of theGettingStarted
property, as well as the event trigger and response that updates the UI.
Summary
Through use of ReactiveUI and AvaloniaUI you can achieve two-way reactivity, responding to events from the UI, and triggering new changes to the UI in turn.