AvaloniaUI Simple Two-Way Reactivity

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 Commandto 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:

  1. Window is opened
  2. ButtonClicked ReactiveCommand property is attached to the Clicked method
  3. Button is clicked by the user
  4. Binding property (ButtonClicked) is accessed
  5. 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:

  1. Window is opened
  2. ButtonClicked ReactiveCommand property is attached to the ChangeGreetingText method
  3. Button is clicked by the user
  4. Binding property (ButtonClicked) is accessed
  5. Attached method (ChangeGreetingText) is called
  6. String Value of GettingStarted is changed
  7. GettingStarted Setter is called
  8. AvalonObject‘s SetValue method is called, passing in an AvalonProperty initialised with our GettingStarted property
  9. The AvalonProperty handles the changing of the string value of the GettingStarted 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.

Leave a comment