Skip to content

UI Coding Conventions and Guidelines

The Nexus Mods App uses Avalonia combined with ReactiveUI and Dynamic Data for the user interface.

This document contains conventions, guidelines, tips and tutorials on how to use these tools in the project.

We'll also be going over some technical details on how these tools work and how they interact with the project.

Avalonia

Avalonia is a multi-platform UI framework for creating native .NET applications.

It takes inspiration from WPF and WinUI but is distinctly different to ensure it works on all platforms. The framework also uses Skia for rendering to ensure cross-platform compatibility.

Understanding XAML Bindings

Describes how data from the code makes its way to the UI.

Avalonia uses AXAML, which has some minor differences compared to the standard XAML that was popularized by WPF.

Simple Introduction

Creating a new view called MyView.axaml will also create a code behind file called MyView.axaml.cs that, by default, only has a InitializeComponent(); call inside it's constructor:

public partial class MyView : UserControl
{
    public MyView()
    {
        InitializeComponent();
    }
}

To actually display any data you need a data context. Every control in Avalonia has a property called DataContext. You can set this property to some instance of a class that contains data that you want to display:

public class MyData
{
    public string Greeting => "Hello World!";
}

public partial class MyView : UserControl
{
    public MyView()
    {
        DataContext = new MyData(); // 👈
        InitializeComponent();
    }
}

Inside the AXAML file of the view, you can now add a TextBlock and bind the MyData.Greeting property to the TextBlock.Text property:

<StackPanel>
    <TextBlock Text="{Binding Greeting}" />
</StackPanel>

This kind of binding is called XAML Binding because the binding is created inside the UI markup file.

The default binding mode for most properties is one way meaning that binding is from the source, aka the data context, to the target (the view).

Another common binding mode is two way binding which is required for properties on input controls, like TextBox.Text and Checkbox.IsChecked.

Working with Avalonia Previewer

When using XAML bindings, it's recommended to set the design data context. This provides a hint to the IDE auto-completion service and allows you to use the previewer.

<Design.DataContext>
    <ui:MyData/>
</Design.DataContext>

Updating the Displayed Content

Currently, the MyData.Greeting property is a get-only property, meaning it doesn't have a setter and the underlying field can't be changed. Let's change the MyData.Getting property to have a public setter:

public class MyData
{
    public string Greeting { get; set; } = "Hello World!";
}

To update the text, we can add a simple button to the view that, when triggered, will change the property to something else:

<StackPanel>
    <TextBlock Text="{Binding Greeting}" />
    <Button Click="OnClick">Change Text</Button>
</StackPanel>

The Button control has a Click event that we can use to register an event handler called OnClick. This event handler will be called whenever the button is clicked:

public partial class MyView : UserControl
{
    public MyView()
    {
        DataContext = new MyData();
        InitializeComponent();
    }

    private void OnClick(object? sender, RoutedEventArgs e)
    {
        DataContext.Greeting = "Hallo Welt!";
    }
}

If you were to build and run this project, you'll find that clicking the button does nothing.

This is because the data context doesn't notify the view that the property has changed.

To add this functionality, the data context needs to implement the INotifyPropertyChanged interface:

public class MyData : INotifyPropertyChanged
{
    private string _greeting = "Hello World!";
    public string Greeting
    {
        get => _greeting;
        set
        {
            _greeting = value;
            OnPropertyChanged(nameof(Greeting));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

This is a lot of boilerplate code that various frameworks and tools can abstract away, but you should be aware of what's going on behind the scenes. In this code snippet, we explicitly implement the property setter to invoke the PropertyChanged event.

If the view has any bindings and the data context implements this interface, the view will register an event handler at runtime to listen for property changes the view binds to. This allows the framework to re-render only a specific part of the UI, since it now knows exactly which parts of the UI have been updated.

Changes to the displayed content must be done on the UI Thread.

How Nexus Mods App Does UI (ReactiveUI)

Events & Bindings

The XAML bindings from the previous example work great for very simple applications.

However... once you start adding more and more functionality to it, developing with XAML bindings can have some massive disadvantages.

The main disadvantage comes from using events. With events you register an event handler, either from the AXAML (providing very little control), or from the code behind, creating a mess and potentially leading to Event Handler Leaks.

Example with Events (from Code Behind)

public YourView()
{
    // Note: This isn't strictly needed for this specific example, as both the event
    //       receiver and publisher are the same view, however not doing this with long
    //       lived objects outside of this view risks event leaks.
    this.Activated += WindowActivated;
    this.Deactivated += WindowDeactivated;
}

private void WindowActivated(object sender, EventArgs e)
{
    this.PropertyChanged += TextPropertyChanged;
}

private void WindowDeactivated(object sender, EventArgs e)
{
    this.PropertyChanged -= TextPropertyChanged;
}

private void TextPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(Text))
    {
        if (Text.Length > 10)
        {
            // Do Stuff
        }
    }
}

An alternative to events for such notifications are observables (observable design pattern). The pattern defines an observable (IObservable<T>) as a provider for push-based notifications and an observer (IObserver<T>) as a mechanism for receiving push-based notifications.

The great thing about this pattern and how it's implemented in C#, is that it doesn't require any special syntax or keywords like the event keyword. Conceptually, these are just interfaces that have Extension Methods that return values.

The result is you can adapt other language constructs like LINQ on the pattern:

Example with Observables

public YourView()
{
    InitializeComponent();
    this.WhenActivated(disposables =>
    {
        // Observables
        this.WhenAnyValue(x => x.Text)
            .Select(text => text.Length > 10)
            .Subscribe(hasMinLength => { })
            .DisposeWith(disposables);
    }
}

This makes the pattern inherently composable and is one of the major reasons why ReactiveUI is commonly used in UI development.

Sadly, ReactiveUI requires a lot of boilerplate code to get started, as well as a base level understanding of various software development concepts like the observable pattern.

Binding Patterns ('Model-View-ViewModel' a.k.a. MVVM)

Most .NET UI Projects, including ours use an architectural pattern called 'MVVM' for managing complex apps.

Another important pattern is the Model-View-ViewModel pattern, or MVVM.

At it's core, the MVVM pattern allows us to separate our various components.

  • View


    The View is what appears on the user's screen. In the previous example, that's MyView.

  • Model


    The Model represents the data and business logic of the application.

    For example a Mod is a model.

  • ViewModel


    The ViewModel, such as MyData, provides public properties and commands for the View to bind to.

    It facilitates communication between the View and the Model.

MVVM and it's separation of concerns makes developing the views and ViewModels straightforward:

  • The View should only bind to the ViewModel.
  • The ViewModel should only contain the functionality required to drive the View and display the data.

ReactiveUI makes this pattern much easier to implement.

An Example

Let's re-create the previous example and change it to use MVVM, ReactiveUI and the observable pattern instead of being event-driven with XAML bindings:

We can start with MyViewModel and a simple Greeting property:

public class MyViewModel
{
    public string Greeting { get; } = "Hello World!";
}

Instead of creating a normal Avalonia UserControl, we can create a ReactiveUserControl that is provided the Avalonia.ReactiveUI package:

<reactive:ReactiveUserControl
    x:TypeArguments="ui:MyViewModel"
    xmlns="https://github.com/avaloniaui"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ui="clr-namespace:Example"
    xmlns:reactive="http://reactiveui.net"
    mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
    x:Class="Example.MyView">

    <Design.DataContext>
        <ui:MyViewModel />
    </Design.DataContext>

    <StackPanel>
        <TextBlock />
    </StackPanel>

</reactive:ReactiveUserControl>

The code-behind class MyView.axaml.cs will also have inherit from ReactiveUserControl<TViewModel>:

public partial class MyView : ReactiveUserControl<MyViewModel>
{
    public MyView()
    {
        InitializeComponent();
    }
}

But why do we need to do this? What's a ReactiveUserControl?

Well, each view has a lifetime, for example opening Window and later closing a window in an App. ReactiveUserControl allows ReactiveUI to 'hook into' Avalonia lifetimes.

This means we can create our resources when a page or window is shown, and dispose them when we're done.

A detailed explanation of under the hood, inner workings of ReactiveUserControl (click to expand)

Resource cleanup is handled by the built-in .NET IDisposable interface.

public interface IDisposable
{
    void Dispose();
}

ReactiveUserControl<TViewModel> inherits from the Avalonia UserControl class but also implements the ReactiveUI interface IViewFor<TViewModel>. This interface extends IActivatableView, which is a marker interface for telling ReactiveUI that the current view can be activated. The activation method is an implementation detail of the UI framework itself, ReactiveUI supports more than Avalonia, so this needs to be kept vague. In the context of Avalonia, activation occurs when the view gets loaded

The Loaded and Unloaded events of an Avalonia Control determine whether a view is activated or not. The code snippet above also showcases that you can construct observables from events and thus convert from event-driven programming to the observable pattern.

The IObservable<T> and IObserver<T> interfaces (observables) are tightly combined with the built-in .NET IDisposable interface used for resource cleanup.

Recall part of the earlier 'Observables' example:

// Method inside 'WhenActivated' is called when Window/Page is shown.
this.WhenActivated(disposables => // 👈 'disposables' is ran on window/page close.
{
    this.WhenAnyValue(x => x.Text)
        .Select(text => text.Length > 10)
        .Subscribe(hasMinLength => { }) // 👈 returns IDisposable
        .DisposeWith(disposables); // 👈 dispose on window/page close
}

The Subscribe method on an IObservable<T> returns IDisposable. Calling Dispose on this returned instance allows the observer to stop receiving notifications from the provider.

The important aspect of this is that the observable (property) will remove the reference to the observer (subscribe method).

Why is this cleanup important?

Garbage Collection (GC).

If you don't dispose the observer, it may hold reference to the observable for a longer period of time, potentially even the entire lifetime of the process. (i.e. Event Handler Leaks but with observables)

Note that in the example above it's not strictly necessary to call DisposeWith due to how Avalonia is built under the hood (unlike WPF, UWP), however risks of leaks are real when working with 'external' (injected) components.

So in the App we dispose everything, for consistency and for safety.

In summary, instances of IObservable<T> and IObserver<T> should always be disposed when they go out of scope.

Adding a Binding

Going back to the code-behind of our view, we currently have no bindings at all:

public MyView()
{
    InitializeComponent();
}

To create a 'reactive' binding, we need to be able to reference the control in our code

This can be done by simply adding a name to the control. In this example, we want to change the text of the TextBlock, so we can attach a name to that control in the view:

<StackPanel>
    <TextBlock x:Name="MyTextBlock" />
</StackPanel>

Avalonia's source generator will generate a field available from .axaml.cs with the same name:

// <auto-generated />
partial class MyView
{
    internal global::Avalonia.Controls.TextBlock MyTextBlock;

    public void InitializeComponent(bool loadXaml = true)
    {
        if (loadXaml)
        {
            AvaloniaXamlLoader.Load(this);
        }

        MyTextBlock = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("MyTextBlock");
    }
}

This is why your .axaml.cs file has to be partial, and why constructor calls InitializeComponent

With the name in place, we can create a one-way bind from the Greeting property in the ViewModel to the Text property of the TextBlock control in our View:

public MyView()
{
    InitializeComponent();

    this.WhenActivated(disposables =>
    {
        this.OneWayBind(ViewModel, vm => vm.Greeting, view => view.MyTextBlock.Text)
            .DisposeWith(disposables);
    });
}

OneWayBind is an extension method of this, with the following definition:

public static IReactiveBinding<TView, TVProp> OneWayBind<TViewModel, TView, TVMProp, TVProp>(
    this TView view,
    TViewModel? viewModel,
    Expression<Func<TViewModel, TVMProp?>> vmProperty,
    Expression<Func<TView, TVProp>> viewProperty,
    object? conversionHint = null,
    IBindingTypeConverter? vmToViewConverterOverride = null)
    where TViewModel : class
    where TView : class, IViewFor);

That method might be a bit daunting to look at, but basically...

  • We pass the ViewModel
  • A property of the ViewModel in a 'lambda' (vm.Greeting)
  • A property of an UI control (ReactiveUserControl) in a 'lambda' (MyTextBlock.Text)

Note the two lambdas are technically expression trees, not methods, some language limitations apply.

It's complex but think of expression trees being telling the framework 'how to get your property' , as opposed to 'make method that gets your property'.

The last two arguments of the OneWayBind function are optional, they mostly exist to convert between one type to another but should only rarely be used.

OneWayBind returns an instance of IReactiveBinding<TView, TValue> which should be disposed with DisposedWith, as mentioned before.

Now the UI is listening to changes in the ViewModel.

If you add INotifyPropertyChanged and made Greeting modifiable by adding set;, the UI should update on value change. But changing UI will not change code behind, as it's a 'one way' binding.

Commands (The Reactive Way)

Our MyViewModel.Greeting property is currently just get-only, which means it will never change.

Let's add a Button that will modify the property (like we did before, but in the 'reactive' way!):

public class MyViewModel
{
    //                updated here 👇
    public string Greeting { get; set; } = "Hello World!";
}
<StackPanel>
    <TextBlock x:Name="MyTextBlock" />
    <Button x:Name="MyButton">Click Me!</Button>
</StackPanel>

Instead of using the Click event, we will use Commands.

Commands allow the View to trigger logic defined in the ViewModel

public class MyViewModel
{
    public string Greeting { get; set; } = "Hello World!";
    public ReactiveCommand<Unit, Unit> ChangeGreetingCommand { get; }

    public MyViewModel()
    {
        ChangeGreetingCommand = ReactiveCommand.Create(() =>
        {
            Greeting = "Hallo Welt!"; // Herro Za Warudo!
        });
    }
}

It's important to note that ReactiveCommand<TParam, TResult> takes in a TParam and returns a TResult.

Usually you won't have inputs/outputs, so you can use Unit type which is essentially an alias for void.

We can now bind with the BindCommand extension method:

this.WhenActivated(disposables =>
{
    this.OneWayBind(ViewModel, vm => vm.Greeting, view => view.MyTextBlock.Text)
        .DisposeWith(disposables);

    // 👆 Code from before
    // 👇 New Code

    this.BindCommand(ViewModel, vm => vm.ChangeGreetingCommand, view => view.MyButton)
        .DisposeWith(disposables);
});

This is just like OneWayBind from earlier, but we select the control itself rather than one of its properties. The framework will figure out the rest 😛.

Compiling and running this, clicking the button will not change the text.

We must not forget to implement INotifyPropertyChanged. This time we won't write the boilerplate however, we'll let Reactive take care of it.

public class MyViewModel : ReactiveObject
{
    [Reactive]
    public string { get; set; } = "Hello World!";
}
public class MyViewModel : ReactiveObject
{
    private string _greeting = "Hello World!";
    public string Greeting
    {
        get => _greeting;
        set => this.RaiseAndSetIfChanged(ref _greeting, value);
    }
}
public class MyData : INotifyPropertyChanged
{
    private string _greeting = "Hello World!";
    public string Greeting
    {
        get => _greeting;
        set
        {
            _greeting = value;
            OnPropertyChanged(nameof(Greeting));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

We've successfully removed a lot of boilerplate this time around. ReactiveObject already implements INotifyPropertyChanged and adding [Reactive] to the property will transform our code to use it.

The [Reactive] attribute is part of the ReactiveUI.Fody package.

Conditional Execution

Another cool feature of ReactiveCommand is the canExecute observable:

public class MyViewModel : ReactiveObject
{
    [Reactive]
    public string Greeting { get; set; } = "Hello World!";

    public ReactiveCommand<Unit, Unit> ChangeGreetingCommand { get; }

    public MyViewModel()
    {
        var canExecute = this
            .WhenAnyValue(vm => vm.Greeting)
            .Select(greeting => greeting == "Hello World!");

        ChangeGreetingCommand = ReactiveCommand.Create(() =>
        {
            Greeting = "Hallo Welt!";
        }, canExecute);
    }
}

When creating a ReactiveCommand, you can pass an IObservable<bool> along.

When the command gets created, it will subscribe to this observable and make the command unavailable if the observable returns false.

If you bind this command to a Button, the framework will disable the button if the command can't execute.

If you are using an observable that references something outside of the current scope, remember to use WhenActivated and DisposeWith on the ReactiveCommand.

Selecting Multiple Properties

When creating commands and the canExecute observable, you might want to check multiple properties at once:

public class MyViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    [Reactive] public string Text { get; private set; } = string.Empty;

    [Reactive] public bool IsChecked { get; set; }

    public ReactiveCommand<Unit, Unit> AddCommand { get; set; }

    public MyViewModel()
    {
        // This command should only be available if Text is not empty and IsChecked is true
        AddCommand = ReactiveCommand.Create(() => { });
    }
}

There are two ways to handle this in Reactive:

var canExecute = this.WhenAnyValue(
    vm => vm.Text,
    vm => vm.IsChecked,
    (text, isChecked) => !string.IsNullOrWhiteSpace(text) && isChecked
);

AddCommand = ReactiveCommand.Create(() => { }, canExecute);
var hasText = this.WhenAnyValue(vm => vm.Text).Select(text => !string.IsNullOrWhiteSpace(text));
var isChecked = this.WhenAnyValue(vm => vm.IsChecked);
var canExecute = hasText.CombineLatest(isChecked).Select(tuple => tuple is { First: true, Second: true });

AddCommand = ReactiveCommand.Create(() => { }, canExecute);

The former is preferred due to code cleanliness.

Make sure to use good variable named with WhenAnyValue, not x, y, z.

Nested Bindings (Expression Chains)

ReactiveUI uses expression trees for methods like OneWayBind and WhenAnyValue.

Expressions can be used to set up "chains" for nested properties:

this.WhenAnyValue(x => x.Foo!.Bar!.Baz)
    .Subscribe(x => Console.WriteLine(x));

You can't use ? for 'null propagation' in expressions, i.e. x => x.Foo?.Bar?.Baz

WhenAny and all it's variants, will only send notifications if evaluating the expression wouldn't throw a NullReferenceException

This is why you use x => x.Foo!.Bar!.Baz, you tell the compiler to ignore nullability of the properties.

ReactiveUI will prevent any null related exceptions and crashes from occuring in these expression chains.

When you do x => x.Foo.Bar.Baz, ReactiveUI will set up the following subscriptions:

  1. Subscribe to this, look for Foo
  2. Subscribe to Foo, look for Bar
  3. Subscribe to Bar, look for Baz
  4. Subscribe to Baz, publish to Subject

This means:

  • If Foo changes, this will be notified and it will re-subscribe to the new Foo.
  • If Bar changes, Foo will be notified and it will re-subscribe to the new Bar.

You don't have to manually manage nested subscriptions, the framework does it for you.

Lastly, WhenAny only notifies on changes of the output value. It only tells you when the final value of the expression has changed. If any intermediate values changed, then the subscriptions will be updated again, but you won't get a new notification if the final value hasn't changed:

this.WhenAnyValue(x => x.Foo!.Bar!.Baz)
    .Subscribe(x => Console.WriteLine(x));

this.Foo.Bar.Baz = "Hi!";
// ✅ "Hi!"

this.Foo.Bar.Baz = "Hi!";
// ❌ Value hasn't changed, nothing happened

this.Foo.Bar = new Bar() { Baz = "Hi!" };
// ❌ The intermediate value changed, but final hasn't.

this.Foo.Bar = new Bar() { Baz = "Hello!" };
// ✅ "Hello!"

Exceptions with ReactiveUI

By default, ReactiveUI will crash the application if an exception is thrown inside a command or an subscription.

this.WhenActivated(disposables =>
{
    this.WhenAnyValue(vm => vm.IsChecked)
        .Where(b => b)
        .Do(_ => ThrowSomething())
        .Subscribe()
        .DisposeWith(disposables);
});

For commands and other objects that have a ThrownExceptions property, you can register a different default exception handler.

For subscriptions, you need to use SubscribeSafe instead of Subscribe:

this.WhenActivated(disposables =>
{
    this.WhenAnyValue(vm => vm.IsChecked)
        .Where(b => b)
        .Do(_ => ThrowSomething())
        .SubscribeSafe(Observer.Create<bool>(
            _ => { }, // onNext
            ex => Console.WriteLine(ex), // onError
            () => { }) // onCompleted
        ).DisposeWith(disposables);
});

SubscribeSafe re-routes synchronous exceptions to the OnError channel of the observer.

Our ReactiveUiExtensions class has SubscribeWithErrorLogging that can be used for convenience:

this.WhenActivated(disposables =>
{
    this.WhenAnyValue(vm => vm.IsChecked)
        .Where(b => b)
        .Do(_ => ThrowSomething())
        .SubscribeWithErrorLogging()
        .DisposeWith(disposables);
});

This uses the injected ILogger<T> and should be used instead of SubscribeSafe.

Binding Collections & Lists (Dynamic Data)

What is Dynamic Data?

Dynamic Data is the glue that allows collections (e.g. lists) to be used 'reactive' environment.

Instead of using events, it provides observers and observables.

Besides the INotifyPropertyChanged, there is also INotifyCollectionChanged used for whole collections as opposed to simple properties.

This interface alongside its CollectionChanged event tells listeners when items are added/removed/replaced etc.

Common implementations of this interface include ObservableCollection<T> and it's read-only counterpart ReadOnlyObservableCollection<T>.


Let's look at an example that displays a bunch of GUIDs using a ListBox.

The user can click on an "Add" button to add a new GUID, they can select an item from the list and they can click a "Remove" button to remove the selected item from the list:

public class MyViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    private readonly SourceList<Guid> _sourceList = new();

    private readonly ReadOnlyObservableCollection<string> _items;

    // 👇 we bind to this from the UI
    public ReadOnlyObservableCollection<string> Items => _items;

    [Reactive] public int SelectedIndex { get; set; } = -1;

    public ReactiveCommand<Unit, Unit> AddCommand { get; }
    public ReactiveCommand<Unit, Unit> RemoveCommand { get; }

    public MyViewModel()
    {
        AddCommand = ReactiveCommand.Create(() =>
        {
            _sourceList.Edit(list => list.Add(Guid.NewGuid()));
        });

        var canRemove = this
            .WhenAnyValue(vm => vm.SelectedIndex)
            .Select(selectedIndex => selectedIndex >= 0 && selectedIndex < _sourceList.Count);

        RemoveCommand = ReactiveCommand.Create(() =>
        {
            _sourceList.Edit(list => list.RemoveAt(SelectedIndex));
        }, canRemove);

        _sourceList
            .Connect() // 👈 we use the code here to make ReadOnlyObservableCollection from DynamicData
            .Transform(guid => guid.ToString()) // 👈 we can sort, manipulate, etc. the items
            .Bind(out _items)
            .Subscribe();
    }
}

All observables in this ViewModel references ViewModel itself, so WhenActivated and DisposeWith are not needed here.

The part of the code that comes from Dynamic Data is the SourceList<T>. There is also SourceCache<TObject, TKey> (essentially a Dictionary) that you should use when your objects have unique identifiers, and you don't care about the position, since you'll likely be sorting them anyways.

When you have a unique key (e.g. for Database Entities like our App), prefer SourceCache.

This example uses a ListBox to display the items and the ListBox.SelectedIndex property to remove an item at a specific index, so we use a SourceList<T>.

Updating DynamicData Collections

This is usually done with the Edit method provided by Dynamic Data

Notably, the Edit method also does "batching" meaning that removing multiple items from the collection in one edit will result in only one notification (IChangeSet), instead of n single item removals.

An Example of Single Notification (Click to Expand)
AddCommand = ReactiveCommand.Create(() =>
{
    Console.WriteLine("Before edit");
    _sourceList.Edit(list =>
    {
        Console.WriteLine("Start of edit");
        list.Add(Guid.NewGuid());
        list.Add(Guid.NewGuid()); //
        Console.WriteLine("End of edit");
    });
    Console.WriteLine("After edit");
});

_sourceList
    .Connect()
    .Transform(guid => guid.ToString())
    .Bind(out _items)
    .Subscribe(_ => Console.WriteLine("In subscription"));

The output is the following:

Before edit
Start of edit
End of edit
In subscription
After edit

If you add another list.Add(Guid.NewGuid());, this output won't change, thanks to batching.

Rendering the Collection

View:

<StackPanel>
    <Button x:Name="AddButton">Add</Button>
    <Button x:Name="RemoveButton">Remove</Button>
    <ListBox x:Name="MyListBox" SelectionMode="Single">
        <ListBox.DataTemplates>
            <DataTemplate DataType="{x:Type system:String}">
                <TextBlock Text="{CompiledBinding}"/> <!-- Bind to DataContext (our string) -->
            </DataTemplate>
        </ListBox.DataTemplates>
    </ListBox>
</StackPanel>

View (Code Behind)

this.WhenActivated(disposables =>
{
    this.BindCommand(ViewModel, vm => vm.AddCommand, view => view.AddButton)
        .DisposeWith(disposables);

    this.BindCommand(ViewModel, vm => vm.RemoveCommand, view => view.RemoveButton)
        .DisposeWith(disposables);

    // Bind to the public Items property, NOT to the SourceCache!
    this.OneWayBind(ViewModel, vm => vm.Items, view => view.MyListBox.ItemsSource)
        .DisposeWith(disposables);
});

A Note on ListBox Rendering (There's a Small Surprise! 🎁)

While we're using reactive bindings to bind to the ListBox.ItemsSource property, the control expects us to provide a DataTemplate that is used to actually render the items. Here we just used a TextBlock and asked it to handle string types.

However, with Reactive, if the item type is a ViewModel, you don't need to use XAML bindings at all! ReactiveUI comes with a built-in feature to auto-create Views from ViewModels!

If you have a ViewModel, the framework can look for all registered Views, construct the matching View, and bind the ViewModel to it.

The following code will scan the entire assembly for Views that implement IViewFor<TViewModel> and associates them with the corresponding TViewModel:

public partial class App
{
    public App()
    {
        Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly());
    }
}

To better understand how this works, let's create a StringView and StringViewModel:

public class StringViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();
    public readonly string Text;

    public StringViewModel(string text)
    {
        Text = text;
    }
}
public partial class StringView : ReactiveUserControl<StringViewModel>
{
    public StringView()
    {
        InitializeComponent();

        this.WhenActivated(disposables =>
        {
            this.WhenAnyValue(view => view.ViewModel)
                .WhereNotNull()
                .Do(PopulateFromViewModel)
                .Subscribe()
                .DisposeWith(disposables);
        });
    }

    private void PopulateFromViewModel(StringViewModel vm)
    {
        MyTextBlock.Text = vm.Text;
    }
}

This is overkill to display a single read-only field but it serves to illustrate how View resolution works.

Also note that the code-behind of the View is slightly different.

If the ViewModel properties don't change over time (they are read only) set the properties directly.

This is more efficient since you only have a subscription on the ViewModel instead of every property.

The MyViewModel.Items collection also has to be updated to use StringViewModel instead of string:

private readonly ReadOnlyObservableCollection<StringViewModel> _items;
public ReadOnlyObservableCollection<StringViewModel> Items => _items;

And the main View can be simplified, with XAML bindings removed:

<StackPanel>
    <Button x:Name="AddButton">Add</Button>
    <Button x:Name="RemoveButton">Remove</Button>
    <ListBox x:Name="MyListBox" SelectionMode="Single" />
</StackPanel>

At runtime, ReactiveUI will find the correct View for the ViewModel and instantiate it. This is done by the ViewModelViewHost control from Avalonia.ReactiveUI package.

It will also give MyListBox a DataTemplate automatically.

For reference, when MyListBox is auto given a DataTemplate it's basically given this (click to expand)
<StackPanel>
    <Button x:Name="AddButton">Add</Button>
    <Button x:Name="RemoveButton">Remove</Button>
    <ListBox x:Name="MyListBox" SelectionMode="Single">
        <DataTemplate>
            <!-- Don't bind from XAML, this is for example only !-->
            <reactive:ViewModelViewHost ViewModel="{CompiledBinding}"/>
        </DataTemplate>
    </ListBox>
</StackPanel>

For reference only, don't do this in the code.

The ViewModelViewHost control should only be used if you can directly bind to the ViewModel property.

This is useful if you have a View that can display potentially any ViewModel, for example in 'container' views.

Nesting ViewModels in ViewModels

Once you start using observable collections with ViewModels you might end up in a scenario where you have a "parent" ViewModel and multiple "child" ViewModels that get created by the parent:

public class MyViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    private readonly SourceCache<ChildViewModel, Guid> _sourceCache = new(child => child.Id);
    private readonly ReadOnlyObservableCollection<ChildViewModel> _children;
    public ReadOnlyObservableCollection<ChildViewModel> Children => _children;

    public readonly ReactiveCommand<Unit, Unit> AddChildCommand;

    public MyViewModel()
    {
        _sourceCache
            .Connect()
            .Bind(out _children)
            .Subscribe();

        AddChildCommand = ReactiveCommand.Create(() =>
        {
            _sourceCache.Edit(updater =>
            {
                updater.AddOrUpdate(new ChildViewModel());
            });
        });
    }
}

public class ChildViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    public Guid Id { get; }
    public string Name { get; }

    public ChildViewModel()
    {
        Id = Guid.NewGuid();
        Name = Id.ToString("D");
    }
}

This can be pretty common but requires some design decisions to be made before continuing.

Parent reacting to changes in one of the children

Example Use Case! Display number of selected children in title/text.

We previously learned that ReactiveUI supports expression chaining using this.WhenAnyValue(x => x.Foo.Bar.Baz) but this only works if each property in this chain is a single item and not a collection.

For this example, let's assume the View for the ChildViewModel contains a CheckBox that is bound to the IsChecked property:

public class ChildViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    public Guid Id { get; }
    public string Name { get; }

    [Reactive]
    public bool IsChecked { get; set; }

    public ChildViewModel()
    {
        Id = Guid.NewGuid();
        Name = Id.ToString("D");
    }
}

The parent wants to be notified when the IsChecked property on any of the children changes.
This can be easily done using Dynamic Data:

this.WhenActivated(disposables =>
{
    _sourceCache
        .Connect()
        .WhenValueChanged(child => child.IsChecked)
        .Subscribe(newValue => Console.WriteLine(newValue))
        .DisposeWith(disposables);
});

Adding a new item to the source cache will print false because that's the initial value. When clicking the checkbox, the console will print true and unchecking the checkbox will print false again. This works for any amount of children.

The extension method WhenValueChanged is part of Dynamic Data and has an optional parameter to change this behavior:

this.WhenActivated(disposables =>
{
    _sourceCache
        .Connect()
        .WhenValueChanged(child => child.IsChecked, notifyOnInitialValue: false)
        .Subscribe(newValue => Console.WriteLine(newValue))
        .DisposeWith(disposables);
});

You can also replace WhenValueChanged with the more powerful version WhenPropertyChanged:

this.WhenActivated(disposables =>
{
    _sourceCache
        .Connect()
        .WhenPropertyChanged(child => child.IsChecked)
        .Subscribe(propertyValue => Console.WriteLine($"Sender: {propertyValue.Sender.Id} Value: {propertyValue.Value}"))
        .DisposeWith(disposables);
});

Instead of only getting the value, you get a tuple that contains the sender and the value.

WhenValueChanged and WhenPropertyChanged create observables and subscriptions on each of the children.
Dispose with with DisposeWith

Children sending notifications to the parent

The previous scenario was about the parent reacting to changes in the child, but what if the child wants to send a notification to the parent?

Let's assume the View of the child has a "Remove" Button that, when clicked, will remove the child from the list.

This requires the child ViewModel to have a reactive command RemoveCommand that is bound to the "Remove" Button. When the RemoveCommand is being executed, it has to tell the parent to remove the child.

This problem has multiple solutions, we will show some of them to illustrate the differences.

The first idea would be to have a RemoveChild method on the parent and pass the parent to the child when it gets instantiated:

// Parent Code
public MyViewModel()
{
    AddChildCommand = ReactiveCommand.Create(() =>
    {
        _sourceCache.Edit(updater =>
        {
            // pass "this" to the child
            updater.AddOrUpdate(new ChildViewModel(this));
        });
    });
}

public void RemoveChild(Guid childId)
{
    // with a source cache we only need an ID to remove the child
    _sourceCache.Edit(updater =>
    {
        updater.Remove(childId);
    });
}

// Child Code
public ChildViewModel(MyViewModel parent)
{
    Id = Guid.NewGuid();
    Name = Id.ToString("D");

    RemoveCommand = ReactiveCommand.Create(() =>
    {
        // simply call the method on the parent
        parent.RemoveChild(Id);
    });
}

Instead of calling a method, we could also have a reactive command:

public MyViewModel()
{
    RemoveChildCommand = ReactiveCommand.Create<Guid>(childId =>
    {
        _sourceCache.Edit(updater =>
        {
            updater.Remove(childId);
        });
    });
}

// pass the RemoveChildCommand directly
public ChildViewModel(ReactiveCommand<Guid, Unit> removeChildCommand)
{
    Id = Guid.NewGuid();
    Name = Id.ToString("D");

    RemoveCommand = ReactiveCommand.Create(() =>
    {
        // the Execute method returns a "cold" observable, that doesn't do anything until
        // someone subscribes to it
        using var disposable = removeChildCommand.Execute(Id).Subscribe();
    });
}

You can pass an IObserver<Guid> to the child ViewModel:

// 👇 this is the IObserver<Guid>
private readonly Subject<Guid> _removeChildSubject = new();

public MyViewModel()
{
    this.WhenActivated(disposables =>
    {
        _removeChildSubject.Subscribe(childId =>
        {
            _sourceCache.Edit(updater =>
            {
                updater.Remove(childId);
            });
        }).DisposeWith(disposables);
    });
}

public ChildViewModel(IObserver<Guid> removeChildObserver) // 👈 _removeChildSubject
{
    Id = Guid.NewGuid();
    Name = Id.ToString("D");

    RemoveCommand = ReactiveCommand.Create(() =>
    {
        removeChildObserver.OnNext(Id);
    });
}

Finally, there a solution that doesn't pass anything to the child:

public ChildViewModel()
{
    Id = Guid.NewGuid();
    Name = Id.ToString("D");

    RemoveCommand = ReactiveCommand.Create(() => Id);
}

This is the preferred solution

The child ViewModel will have a remove command that does nothing because removing the child is responsibility of the parent.

The parent can make use of the fact that reactive commands also implement IObservable<TResult>, meaning the parent can subscribe to the remove command of the child and do something when the command finished executing:

this.WhenActivated(disposables =>
{
    _sourceCache
        .Connect()
        .MergeMany(child => child.RemoveCommand)
        .Subscribe(childId =>
        {
            _sourceCache.Edit(updater =>
            {
                updater.Remove(childId);
            });
        })
        .DisposeWith(disposables);
});

The star of this solution is MergeMany which is part of Dynamic Data and merges the selected observable on each item. It automatically handles subscriptions for items being added and removed. This code is ideal because it keeps the child stupid and simple.

Trees

If you need to make trees which could potentially have many items, use TreeDataGrid instead.

Dynamic Data also supports trees without tree structures. The data internally is flat. As an example, we'll create a PersonViewModel:

public class PersonViewModel : ReactiveObject, IActivatableView
{
    public ViewModelActivator Activator { get; } = new();

    public Guid Id { get; }
    public string Name { get; }
    public Guid ParentId { get; }

    public ReactiveCommand<Unit, Guid> RemoveCommand { get; }

    public PersonViewModel(Guid id, Guid parentId)
    {
        Id = id;
        ParentId = parentId;
        Name = id.ToString("D");

        RemoveCommand = ReactiveCommand.Create(() => id);
    }
}

The important bit is the fact that the object doesn't contain a reference to other objects, it just has an ID (Guid in this case) that links them together.

public class MyViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    private readonly SourceCache<PersonViewModel, Guid> _sourceCache = new(x => x.Id);
    private readonly ReadOnlyObservableCollection<NodeViewModel> _nodes;
    public ReadOnlyObservableCollection<NodeViewModel> Nodes => _nodes;

    public MyViewModel()
    {
        _sourceCache
            .Connect()
            .TransformToTree(item => item.ParentId) // 👈 right here!
            .Transform(node => new NodeViewModel(node))
            .Bind(out _nodes)
            .Subscribe();

        this.WhenActivated(disposables =>
        {
            _sourceCache
                .Connect()
                .MergeMany(child => child.RemoveCommand)
                .Subscribe(childId =>
                {
                    _sourceCache.Edit(updater =>
                    {
                        updater.Remove(childId);
                    });
                })
                .DisposeWith(disposables);
        });
    }
}

The magic method is TransformToTree from Dynamic Data.

This single method transforms our input into a fully recursive tree using a pivot point we specify (in this case, ParentId).

TransformToTree transforms the IObservable<IChangeSet<PersonViewModel, Guid>> into an IObservable<IChangeSet<Node<PersonViewModel, Guid>, Guid>>.

This Node<TObject, TKey> type comes from Dynamic Data and needs to be transformed into a custom ViewModel for Avalonia:

public class NodeViewModel : ReactiveObject, IActivatableViewModel
{
    public ViewModelActivator Activator { get; } = new();

    private readonly ReadOnlyObservableCollection<NodeViewModel> _children;
    public ReadOnlyObservableCollection<NodeViewModel> Children => _children;

    public PersonViewModel Item { get; }

    public NodeViewModel(Node<PersonViewModel, Guid> node)
    {
        Item = node.Item;

        node.Children
            .Connect()
            .Transform(child => new NodeViewModel(child))
            .Bind(out _children)
            .Subscribe();
    }
}

We convert Node<TObject, TKey> into a NodeViewModel to be able to display them in Avalonia using a TreeView:

<ScrollViewer Background="White">
    <TreeView x:Name="MyTreeView">
        <TreeView.ItemTemplate>
            <TreeDataTemplate DataType="{x:Type ui:NodeViewModel}" ItemsSource="{CompiledBinding Children}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{CompiledBinding Item.Name}" />
                    <Button Command="{CompiledBinding Item.RemoveCommand}">Remove</Button>
                </StackPanel>
            </TreeDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</ScrollViewer>

The code-behind only has to bind to the ItemsSource property of the TreeView and it just works out-of-the-box:

this.WhenActivated(disposables =>
{
    this.OneWayBind(ViewModel, vm => vm.Nodes, view => view.MyTreeView.ItemsSource)
        .DisposeWith(disposables);
});

Trees with Columns (TreeDataGrid)

Functional Example(s) available at TreeDataGrid Examples

Basic TreeDataGrid

Functional Example(s) available at TreeDataGrid Examples as 'Basic'

To create a TreeDataGrid, we will take the following steps:

Update Your ViewModel

Suppose you have a ViewModel in which you want to display the name of a file:

// Something like this.
public interface IFileViewModel : IViewModelInterface
{
    public string Name { get; }
}

In order to use your ViewModel with the TreeDataGrid, you need to implement a few additional interfaces:

public interface IFileViewModel : 
    IViewModelInterface, // For INotifyPropertyChanged and Reactive.
    IExpandableItem, // For ability to expand. (IsExpanded)

    // For child/parent creation via DynamicData.
    // Pass 'self' and 'unique key 'as generic types.
    // This key can be a GUID, auto incremented int, or something else unique.
    // In this example, we use a file/folder path.
    IDynamicDataTreeItem<IFileViewModel, GamePath> 
{
    // Name of the file
    public string Name { get; }
}
Interface Name Purpose
IExpandableItem Provides IsExpanded, for expanding nodes.
IDynamicDataTreeItem Simplifies tree creation with Dynamic Data.

You can now implement this interface as such:

// FullPath: Contains File Path
public class FileDesignViewModel(GamePath fullPath) // 👈 Constructor
    : AViewModel<IFileViewModel>, IFileViewModel
{
    // Name of the file
    public string Name => Key.Name;

    // IDynamicDataTreeItem
    public ReadOnlyObservableCollection<IFileViewModel>? Children { get; set; }
    public IFileViewModel? Parent { get; set; }
    public GamePath Key { get; set; } = fullPath;

    // IExpandableViewModel
    [Reactive] public bool IsExpanded { get; set; }
}
Prepare The Data

This is almost identical to previous Trees section.

So we'll keep it simple.

Now we will create a bunch of IFileViewModel objects and expose them as a ReadOnlyObservableCollection the TreeDataGrid can bind to.

// Create the DynamicData container.
var cache = new SourceCache<IFileViewModel, GamePath>(x => x.Key);

// Add some `IFileViewModel` elements here... e.g. from Database //

// Convert to ReadOnlyObservableCollection.
cache.Connect()
    .TransformToTree(model => model.Key.Parent)
    .Transform(node => node.Item.Initialize(node))
    .Bind(out var items)
    .Subscribe(); // force evaluation

We are operating under the assumption that no 2 files/folders can have the same name.

Here we use x => x.Key as the key selector, where Key is the unique identifier for the item. This Key in our example is the file/folder path, which is guaranteed to be unique in a file tree. To identify the parent, we use model.Key.Parent.

Lastly, we do .Transform(node => node.Item.Initialize(node)). This is a extension method which unwraps the DynamicData's Node type, and basically initializes the elements inherited from IDynamicDataTreeItem.

Create the Columns

We take the items created from TransformToTree and create a TreeDataGridSource.

Create a TreeDataGridSource using the items from TransformToTree (previous step):

TreeSource = CreateTreeSource(items);

In the source, we define a single column:

private static HierarchicalTreeDataGridSource<IFileViewModel> CreateTreeSource(
    ReadOnlyObservableCollection<IFileViewModel> treeRoots)
{
    return new HierarchicalTreeDataGridSource<IFileViewModel>(treeRoots)
    {
        Columns =
        {
            new HierarchicalExpanderColumn<IFileViewModel>( // For expanding
                // Our column. 
                // First generic parameter is Input (IFileViewModel)
                // Second generic parameter is Output (something with ToString)
                new TextColumn<IFileViewModel,string>(
                        "Name", // Header name
                        x => x.Name, // Method to return string
                        width: new GridLength(1, GridUnitType.Star)
                    ),
                    node => node.Children, // Getter for children
                    null,
                    node => node.IsExpanded
                ),
        },
    };
}

Add The TreeDataGrid

Add the TreeDataGrid to your View:

<!-- Some elements omitted for clarity -->
<reactiveUi:ReactiveUserControl x:TypeArguments="basic:IFileTreeViewModel"
                                x:Class="Examples.TreeDataGrid.Basic.FileTreeView">
    <Design.DataContext>
        <basic:FileTreeDesignViewModel /> <!-- Preview Data -->
    </Design.DataContext>

    <!-- Visual Tree -->
    <TreeDataGrid Classes="TreeWhiteCaret" ShowColumnHeaders="True"
                  x:Name="ModFilesTreeDataGrid" Width="1"/>

</reactiveUi:ReactiveUserControl>

And bind to the HierarchicalTreeDataGridSource (TreeSource) in code-behind:

public partial class FileTreeView : ReactiveUserControl<IFileTreeViewModel>
{
    public FileTreeView()
    {
        InitializeComponent();

        this.WhenActivated(disposables =>
        {
            // Bind `TreeSource` from previous step to control.
            this.OneWayBind<IFileTreeViewModel, FileTreeView, ITreeDataGridSource<IFileViewModel>, ITreeDataGridSource>
                (ViewModel, vm => vm.TreeSource, v => v.ModFilesTreeDataGrid.Source!)
                .DisposeWith(disposables);

            // This is a workaround for TreeDataGrid collapsing Star sized columns.
            // This forces a refresh of the width, fixing the issue.
            ModFilesTreeDataGrid.Width = double.NaN;
        });
    }
}

The final result should look something like:

TreeDataGrid with Custom Column

Functional Example(s) available at TreeDataGrid Examples as 'Single Column'

This example is stripped down as much as possible, use the examples for full example.

To add a custom column, we will take the following steps:

ViewModel

We extended the ViewModel to include a flag to toggle folder/file icon.

public interface IFileColumnViewModel : IViewModelInterface, 
    IExpandableItem, IDynamicDataTreeItem<IFileColumnViewModel, GamePath>
{
    // New: IsFile dictates whether to show folder or file icon.
    bool IsFile { get; }
    string Name { get; }
}
View

We can now create a custom view for our column.

<!-- Omitted some code for clarity, see examples for full code -->
<reactiveUi:ReactiveUserControl 
    x:TypeArguments="fc:IFileColumnViewModel"
    x:Class="Examples.TreeDataGrid.SingleColumn.FileColumn.FileColumnView"
    d:DataContext="{x:Static fc:FileColumnDesignViewModel.SampleFolder}">
    <Grid Grid.Column="0" ClipToBounds="True" ColumnDefinitions="Auto,Auto" 
          Name="FileElementGrid">
        <!-- File / Directory Icon -->
        <unifiedIcon:UnifiedIcon Grid.Column="0" x:Name="EntryIcon" />
        <!-- File Name -->
        <TextBlock Grid.Column="1" x:Name="FileNameTextBlock" />
    </Grid>
</reactiveUi:ReactiveUserControl>

FileColumnView.axaml.cs

public partial class FileColumnView : ReactiveUserControl<IFileTreeNodeViewModel>
{
    public FileColumnView()
    {
        InitializeComponent();

        // Use `ViewModel.WhenAnyValue` in here if this stuff needs updating 
        // dynamically. There's a separate example for this in 'examples' 😉
        this.WhenActivated(d =>
        {
            // Set the icon.
            // `File` or `FolderOutline` are defined as 'icon styles'
            // (IconsStyles.xaml)
            EntryIcon.Classes.Add(ViewModel!.IsFile ? "File" : "FolderOutline");

            // Set the text.
            FileNameTextBlock.Text = ViewModel!.Name;
        });
    }
}
Using the Custom Column with TreeDataGrid

To use the custom column, update TreeDataGrid.Resources in the View:

<TreeDataGrid Grid.Row="2" Classes="TreeWhiteCaret" ShowColumnHeaders="True">
    <TreeDataGrid.Resources>
        <!-- Specify the following:
            1. View for Custom Columns (`FileColumnView`).
                - The custom view you created just now.
            2. Key for the Custom Column (`CustomRow`).
                - We will reference this in code behind.
        -->
        <DataTemplate x:Key="CustomRow" DataType="{x:Type files:IFileColumnViewModel}">
            <files:FileColumnView DataContext="{CompiledBinding}" />
        </DataTemplate>
    </TreeDataGrid.Resources>
</TreeDataGrid>

And where you're creating the HierarchicalTreeDataGridSource (in CreateTreeSource).

new HierarchicalExpanderColumn<IFileColumnViewModel>(
    new TemplateColumn<IFileColumnViewModel>(
        Language.Helpers_GenerateHeader_NAME, // column name
        "CustomRow", // 👈 specify the custom column 'key'
        // width etc. //
    ),
    node => node.Children,
    null,
    node => node.IsExpanded),

You use a TemplateColumn with your ViewModel.

Stock columns like TextColumn will have default sorting implementations

In this scenario, the names/folders are sorted lexicographically (alphabetically, like in a dictionary).

Tips & Tricks

Bug: Columns Are Incorrectly Sized

When placed in certain containers, columns of the TreeDataGrid may be incorrectly sized when using * width.

This is a bug in the TreeDataGrid control, which is mostly common in single column setups (although can happen in multi column). This bug is fixed by triggering a forced column size recalculation.

An easy way to do so is the following...

  1. Give your grid a positive width in the XAML:

    <TreeDataGrid x:Name="ModFilesTreeDataGrid" Width="1"> <!-- 👈 Set a width to positive number -->
    
  2. Reset the width in the code-behind after assigning a source:

    public YourViewWithGrid()
    {
        InitializeComponent();
        this.WhenActivated(disposables =>
        {
            // Do something to update the TreeDataGrid Source
            this.WhenAnyValue(view => view.ViewModel)
                .WhereNotNull()
                .Do(PopulateFromViewModel)
                .Subscribe()
                .DisposeWith(disposables);
    
            // Reset the control width back to NaN, to default it back as if no
            // width was specified.
            ModFilesTreeDataGrid.Width = double.NaN;
        });
    }
    

When the width is reset, columns are recalculated.

Custom Sorting Rules

Custom sorting can be specified as part of the column definition.

new HierarchicalExpanderColumn<IFileColumnViewModel>(
    new TemplateColumn<IFileColumnViewModel>(
        "NAME",
        "FileNameColumnTemplate",
        width: new GridLength(1, GridUnitType.Star),
        // 👇 Right here
        options: new TemplateColumnOptions<IFileColumnViewModel>
        {
            // Compares if folder first, such that folders show first, then by file name.
            CompareAscending = (x, y) =>
            {
                if (x == null || y == null) return 0;
                var folderComparison = x.IsFile.CompareTo(y.IsFile);
                return folderComparison != 0 ? folderComparison : string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
            },

            CompareDescending = (x, y) =>
            {
                if (x == null || y == null) return 0;
                var folderComparison = x.IsFile.CompareTo(y.IsFile);
                return folderComparison != 0 ? folderComparison : string.Compare(y.Name, x.Name, StringComparison.OrdinalIgnoreCase);
            },
        }
    ),
    node => node.Children,
    null,
    node => node.IsExpanded
)

The example sort above gives you the File Explorer sort. Folders first, then files. Each group is sorted by name.

Sorting Out of the Box

Sometimes you might want to have your tree sorted out of the box.

For example, if you are making a 'file explorer' you want to have the folders on top and files on the bottom by default.

// TreeSource is `HierarchicalTreeDataGridSource`
TreeSource = CreateTreeSource(_items);
TreeSource.SortBy(TreeSource.Columns[0], ListSortDirection.Ascending);

This API isn't easy to discover, so there you go :P

NexusMods.App.UI

This section will be completely about the specifics of the project and the differences to a "normal" Avalonia UI project.

The project NexusMods.App.UI was built with dependency injection in mind. We have a custom View locator that using the DI system called InjectedViewLocator. As mentioned previously, ReactiveUI can construct a View from a ViewModel.

Adding Views

Typically this is done using an assembly scanner that looks for IViewFor<TViewModel> implementations and links them to the TViewModel type. When we request a View for TViewModel, the framework knows that IViewFor<TViewModel> exists and tries to construct it using the default constructor.

In this project, however, the Views are created using DI, meaning that you have to register the Views and ViewModel beforehand in the Services file:

.AddView<MyView, IMyViewModel>()
.AddViewModel<MyViewModel, IMyViewModel>()

For AddView, the View has to implement IViewFor<TViewModel>. You will get this interface for free by using ReactiveUserControl<TViewModel> or ReactiveWindow<TViewModel>.

The actual ViewModel being referenced is an interface.
The interface has to extend IViewModelInterface which is a marker (dummy) interface:

public interface IMyViewModel : IViewModelInterface
{
    public string Name { get; set; }
}

The implementation of this interface MyViewModel would look like this:

public class MyViewModel : AViewModel<IMyViewModel>, IMyViewModel
{
    [Reactive]
    public string Name { get; set; } = string.Empty;
}

The abstract class AViewModel<TViewModel> and the IViewModelInterface are custom made to be used in our DI system. AViewModel<TViewModel> inherits from ReactiveObject and implements IActivatableViewModel, so you can freely use this.WhenActivated inside the constructor.

The benefit of using an interface for the TViewModel type parameter is being able to have different implementations. You'll usually find two implementations in the project, one for design time and another for runtime:

public class MyViewModel : AViewModel<IMyViewModel>, IMyViewModel
{
    [Reactive]
    public string Name { get; set; } = string.Empty;
}

public class MyDesignViewModel : AViewModel<IMyViewModel>, IMyViewModel
{
    public string Name { get; set; } = "This is some design default value";
}

We can use the design ViewModel by setting the design data context in the View:

<Design.DataContext>
    <local:MyDesignViewModel />
</Design.DataContext>

Using a design ViewModel is great if the ViewModel contains only data and next to no logic or commands. You should be mindful of not replicating any logic of the normal ViewModel inside the design ViewModel, as it often results in duplicate, messy and/or less maintainable code.

The design ViewModel can also inherit from the normal ViewModel if that makes it easier.

In summary, you'll need to create four (+1 code-behind) files in most cases:

  • IMyViewModel: extends IViewModelInterface
  • MyView: inherits from ReactiveUserControl<IMyViewModel>
  • MyViewModel: inherits from AViewModel<IMyViewModel> and implements IMyViewModel
  • MyDesignViewModel: inherits from AViewModel<IMyViewModel> and implements IMyViewModel

These files should be grouped together in a folder that isn't a namespace provider. You should also link the ViewModel implementations to the interface, similar to how the code-behind file is linked to the View:

|
|- IMyViewModel.cs
|--- MyViewModel.cs
|--- MyDesignViewModel.cs
|- MyView.axaml
|--- MyView.axaml.cs

This is, understandably, quite a lot of boilerplate just to create a new View, whew!

Best Practices

Threading

Always set properties in the ViewModel on the UI thread. The Views should always act on the UI thread.

Naming

Always use descriptive variables in expressions:

// bad:
this.WhenAnyValue(x => x.Text)
    .Where(x => Find(x))
    .Select(x => !x.Status)
    .Subscribe(x => { });

// good:
this.WhenAnyValue(vm => vm.Text)
    .Where(text => Find(text))
    .Select(searchResult => !searchResult.Status)
    .Subscribe(searchResult => { });

Good alternatives to x:

  • vm for ViewModels
  • view for Views
  • item for items in a collection
  • child for children
  • The name of the property that was previously selected

ViewModel Properties

Always use ObservableAsPropertyHelper<T> (OAPH) to expose the latest values from an IObservable<T> that is async or runs on the task pool scheduler. The OAPH is scheduled by default meaning it doesn't set the value immediately.

This is why it's suited for setting the result of asynchronous work or work that ran on another scheduler:

public class BadExampleViewModel
{
    [Reactive]
    public string Text { get; set; } = string.Empty;

    public BadExampleViewModel()
    {
        this.WhenActivated(disposables =>
        {
            // Don't use Subscribe to the set the property
            Observable
                .FromAsync(SomeAsyncMethod, RxApp.TaskpoolScheduler)
                .Subscribe(text => Text = text)
                .DisposeWith(disposables);
        });
    }

    private static Task<string> SomeAsyncMethod(CancellationToken cancellationToken)
    {
        return Task.FromResult("Hi!");
    }
}

public class GoodExampleViewModel
{
    private readonly ObservableAsPropertyHelper<string> _text;
    public string Text => _text.Value;

    public GoodExampleViewModel()
    {
        _text = Observable
            .FromAsync(SomeAsyncMethod, RxApp.TaskpoolScheduler)
            // Always set the scheduler to be the Main Thread Scheduler
            .ToProperty(this, vm => vm.Text, scheduler: RxApp.MainThreadScheduler);

        this.WhenActivated(disposables =>
        {
            Disposable.Create(() => _text.Dispose()).DisposeWith(disposables);
        });
    }

    private static Task<string> SomeAsyncMethod(CancellationToken cancellationToken)
    {
        return Task.FromResult("Hi!");
    }
}

In a ViewModel, always use BindToVM and a reactive property to expose the latest values from an IObservable<T> that returns immediately and isn't doing anything async:

public class GoodExampleViewModel
{
    [Reactive] public string Text { get; private set; }

    public GoodExampleViewModel()
    {
        this.WhenActivated(disposables =>
        {
            Observable
                .Return("Hi!")
                .BindToVM(this, vm => vm.Text)
                .DisposeWith(disposables);
        });
    }
}

Creating and using Design ViewModels

The design ViewModel should only exist to feed dummy data to the base ViewModel. It can inherit from the normal ViewModel and set properties in the constructor:

interface IMyViewModel : IViewModelInterface
{
    public MyData Data { get; }
}

public class MyViewModel : AViewModel<IMyViewModel>, IMyViewModel
{
    public MyData Data { get; }

    // The base ViewModel doesn't have a default constructor.
    public MyViewModel(MyData data)
    {
        Data = data;
    }
}

public class MyDesignViewModel : MyViewModel
{
    // To be used as a design context, the design ViewModel has to have a default
    // constructor.
    public MyDesignViewModel() : base(GenerateData()) { }

    private static MyData GenerateData() { /* omitted */ }
}

The design ViewModel should be as small and compact as possible. It should also behave the same as the base ViewModel. The only reason it exist is to populate the base ViewModel with dummy data to be rendered in the previewer.

If the base ViewModel creates multiple child ViewModels, then the design ViewModel should not create child design ViewModels

public interface IMyViewModel : IViewModelInterface
{
    public ReadOnlyObservableCollection<IChildViewModel> Children { get; }
}

public interface IChildViewModel : IViewModelInterface { /* omitted */ }
public class MyViewModel : AViewModel<IMyViewModel>, IMyViewModel
{
    private readonly SourceCache<MyData, Guid> _sourceCache = new(x => x.Id);

    private readonly ReadOnlyObservableCollection<IChildViewModel> _children;
    public ReadOnlyObservableCollection<IChildViewModel> Children { get; }

    public MyViewModel(List<MyData> data)
    {
        // The base ViewModel uses the data to create intances of ChildViewModel
        _sourceCache
            .Connect()
            .Transform(item => new ChildViewModel(item))
            .Bind(out _children);

        _sourceCache.Edit(updater =>
        {
            updater.AddOrUpdate(data);
        });
    }
}

public class MyDesignViewModel : MyViewModel
{
    // Instead of using the data to create instances of ChildDesignViewModel,
    // the MyDesignViewModel will only generate dummy data and pass it to MyViewModel.
    public MyDesignViewModel : base(GenerateData()) { }

    private static List<MyData> GenerateData() { /* omitted */ }
}

View to ViewModel Bindings

Always populate the View directly with values from the ViewModel if the values don't change over time (eg. if they are read-only):

public MyView()
{
    InitializeComponent();

    this.WhenActivated(disposables =>
    {
        this.WhenAnyValue(view => view.ViewModel)
            .WhereNotNull()
            .Do(PopulateFromViewModel)
            .Subscribe()
            .DisposeWith(disposables);
    });
}

private void PopulateFromViewModel(MyViewModel vm)
{
    MyTextBlock.Text = vm.Text;
}

Always use OneWayBind if the property can't be changed from the View:

this.WhenActivated(disposables =>
{
    this.OneWayBind(ViewModel, vm => vm.Text, view => view.MyTextBlock.Text)
        .DisposeWith(disposables);
});

Always use Bind if the property is intended to be changed from the View:

this.WhenActivated(disposables =>
{
    this.Bind(ViewModel, vm => vm.IsChecked, view => view.MyCheckBox.IsChecked)
        .DisposeWith(disposables);
});

Always use BindToView to expose the latest value of an observable stream to the View:

this.WhenActivated(disposables =>
{
    this.WhenAnyValue(this, view => view.ViewModel!.SomeFloat)
        .Select(f => f.ToString("###0.00"))
        .BindToView(this, view => view.MyTextBlock.Text)
        .DisposeWith(disposables);
});

Disposing

Always use DisposeWith to dispose any reactive bindings:

this.WhenActivated(disposables =>
{
    this.OneWayBind(ViewModel, vm => vm.Text, view => view.MyTextBlock.Text)
        .DisposeWith(disposables);
});

Always dispose subscriptions of observables:

public class FooViewModel : ReactiveObject, IActivatableViewModel
{
    [Reactive] public BarViewModel? Other { get; set; }

    public FooViewModel()
    {
        this.WhenActivated(disposables =>
        {
            // This observable references "Other" which has an unknown lifetime.
            this.WhenAnyValue(vm => vm.Other!.Text)
                .Subscribe(text => { })
                .DisposeWith(disposables);
        });
    }
}

public class BarViewModel : ReactiveObject, IActivatableViewModel
{
    [Reactive] public string Text { get; set; } = string.Empty;

    public BarViewModel()
    {
        this.WhenActivated(disposables =>
        {
            // This observable references the ViewModel itself, it technically doesn't
            // have to be disposed to be cleaned up as no references to other
            // objects are being created. However, to keep our code uniform and easier
            // to read, you must dispose the subscription, even if it doesn't make a difference.
            this.WhenAnyValue(vm => vm.Text)
                .Subscribe(text => { })
                .DisposeWith(disposable);
        });
    }
}

Dynamic Data

Never expose the SourceList<T> or SourceCache<TObject, TKey> field to the View. These fields should always be marked as private readonly and the only public properties should either be an IObservable<IChangeSet<T>> that is the result from calling .Connect or ObservableCollection<T> or ReadOnlyObservableCollection<T>.

Always try using SourceCache<TObject, TKey> first. If the object type doesn't have an Id, you can always just add one using Guid. SourceList<T> has less APIs and features, like the ones shown in the following examples.

For a parent reacting to changes in any of the children, use WhenValueChanged when you only need the value and WhenPropertyChanged when you also need the sender (example):

public class ChildViewModel
{
    // child has a reactive property that the parent wants to observe changes on:
    [Reactive] public bool IsChecked { get; set; }
}

public ParentViewModel()
{
    this.WhenActivated(disposables =>
    {
        _sourceCache
            .Connect()
            // WhenValueChanged and WhenPropertyChanged return a single IObservable<T> for all items
            // they automatically handle new and removed items
            .WhenValueChanged(child => child.IsChecked)
            .Subscribe(newValue => Console.WriteLine(newValue))
            .DisposeWith(disposables);
    });
}

Instead of passing a reference of the parent to the child, keep the child simple and stupid and have the parent subscribe to an observable of all children using MergeMany (example):

public ChildViewModel()
{
    RemoveCommand = ReactiveCommand.Create(() => Id);
}

public ParentViewModel()
{
    this.WhenActivated(disposables =>
    {
        _sourceCache
            .Connect()
            // ReactiveCommand<TParam, TResult> implements IObservable<TResult>
            .MergeMany(child => child.RemoveCommand)
            .Subscribe(childId =>
            {
                _sourceCache.Edit(updater =>
                {
                    updater.Remove(childId);
                });
            })
            .DisposeWith(disposables);
    });
}