Avalonia More Advanced Button Binding

In Avalonia Buttons Multiple Ways I went through a tour of the various ways one can bind buttons to commands and events. I wanted to explore the topic just a little deeper and look at some of the more advanced scenarios of dealing with binding behavior, specifically passing parameters to commands and setting the button’s enabled status based on other data in the View and View Model. Let’s build a little app to show how to do this, but you can find the final solution for this blog post in this Gitlab Repository.:

For this application example we are going to want the following behaviors: * The application is supposed to generate a simple “Hello World” style greeting when a button is pushed. * One button will use a user editable “Name field” for the greeting. That button is only available if a name is typed however * One button will generate a generic greeting * A checkbox will allow the user to enable/disable whether a generic greeting can be generated

The application when completed will look like the following:

Advanced Button Binding App Screenshot
Application showing binding button state to other data and passing parameters to bound commands


The XAML for the above View is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:AdvancedButtonBinding.ViewModels;assembly=AdvancedButtonBinding"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="200"
        Width="450" Height="200"
        x:Class="AdvancedButtonBinding.Views.MainWindow"
        Icon="/Assets/avalonia-logo.ico"
        Title="AdvancedButtonBinding">

    <Design.DataContext>
        <vm:MainWindowViewModel />
    </Design.DataContext>

    <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto"
          VerticalAlignment="Center">
        <Grid.Styles>
            <Style Selector="Button">
                <Setter Property="Margin" Value="3" />
            </Style>
            <Style Selector="TextBox">
                <Setter Property="Margin" Value="3" />
            </Style>
        </Grid.Styles>
        <TextBlock Grid.Row="0"
                   Text="Name:" />
        <TextBox Grid.Row="1"
                 Text="{Binding Name}" />
        <TextBlock Grid.Row="2"
                   Text="Greeting:" />
        <TextBox Grid.Row="3"
                 Text="{Binding Greeting}" />
        <Button Grid.Row="4"
                Content="Write Greeting"
                Command="{Binding WriteGreetingReactiveCommand}"
                CommandParameter="{Binding Name}" />
        <Button Grid.Row="5"
                Content="Generic Greeting"
                Command="{Binding WriteGreeting}"
                CommandParameter="User"
                IsEnabled="{Binding #AllowedGreeting.IsChecked}" />
        <CheckBox Grid.Row="6"
                  Name="AllowedGreeting"
                  IsChecked="True"
                  Content="Allowed Generic Greeting" />
    </Grid>

</Window>

…and the corresponding View Model is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Text;
using ReactiveUI;

namespace AdvancedButtonBinding.ViewModels
{
    public class MainWindowViewModel : ViewModelBase
    {
        private string greeting;
        public string Greeting
        {
            get => greeting;
            set => this.RaiseAndSetIfChanged(ref greeting, value);
        }
        
        private string name;
        public string Name
        {
            get => name;
            set => this.RaiseAndSetIfChanged(ref name, value);
        }

        public MainWindowViewModel()
        {
            var buttonEnabled = this.WhenAnyValue(
                x => x.Name,
                x => !string.IsNullOrWhiteSpace(x)); 
            WriteGreetingReactiveCommand = ReactiveCommand.Create<string>(
                name => { WriteGreeting(name); }, 
                buttonEnabled);
        }

        public ReactiveCommand WriteGreetingReactiveCommand { get; }

        public void WriteGreeting(string personsName)
        {
            Greeting = $"Hello {personsName}!";   
        }
    }
}

Let’s look at these pieces in isolation.

Toggling Button State Dynamically

There are many instances where we don’t want a button to be active unless certain preconditions are met. A button can be enabled and disabled via the IsEnabled property. Let’s look at setting that up with a binding to the CheckBox’s state (see the Avalonia Documentation on Bindings to Controls for more details). You can see on line 42 of the XAML that the “Generic Greeting” button’s IsEnabled property is set to {Binding #AllowedGreeting.IsChecked}. The CheckBox’s name is “AllowedGreeting” and it has a property called IsChecked that we want to have connected. In Avalonia the hashtag tells the Binding code to look for a control by that name when attempting to do the binding. This simple setting therefore makes it so that the “Generic” button’s state is synchronized with the checkbox

Passing Parameters

Many of the commands we see take no arguments but that’s not always the case. For times where you need that there is an property called CommandParameter which can be used for passing data to the command itself. Let’s look at the generic button first:

<Button Grid.Row="5"
        Content="Generic Greeting"
        Command="{Binding WriteGreeting}"
        CommandParameter="User"
        IsEnabled="{Binding #AllowedGreeting.IsChecked}" />

As we can see we are binding this button to the WriteGreeting method on the View Model.

public void WriteGreeting(string personsName)
{
    Greeting = $"Hello {personsName}!";   
}

So this method is expecting a string argument for the name it’s going to write. In the case of the Generic name we are just going to put in a generic User string which is why the CommandParameter field is set to that in the definition above. However just like everything else it too can be bound to a dynamic data source. Like we did with the regular “Write Greeting” button:

<Button Grid.Row="4"
        Content="Write Greeting"
        Command="{Binding WriteGreetingReactiveCommand}"
        CommandParameter="{Binding Name}" />

Here the CommandParemeter is bound to the Name property on the View Model instead. Recall further up that the TextBox where we enter a name is also bound to the same Name property. So when the button is activated the Name field is read and passed into the calling function. However we want to have that button be disabled unless there is a name there. We could potentially do that all the same way we did the checkbox but the ReactiveCommand will do that for us

Reactive Command Button State Validation

As stated above we have a requirement for having a disabled greeting button unless there is a name there. We can pass a validator to the ReactiveCommand which will automatically toggle the button state for us based on whether the validation comes back true or false. The button binds to the ReactiveCommand WriteGreetingReactiveCommand which is configured for this behavior. Let’s look at that code more carefully:

1
2
3
4
5
6
7
8
9
10
11
public MainWindowViewModel()
{
    var buttonEnabled = this.WhenAnyValue(
        x => x.Name,
        x => !string.IsNullOrWhiteSpace(x)); 
    WriteGreetingReactiveCommand = ReactiveCommand.Create<string>(
        name => { WriteGreeting(name); }, 
        buttonEnabled);
}

public ReactiveCommand WriteGreetingReactiveCommand { get; }

As we did in our first example we defined a new ReactiveCommand property so the View can bind to it in the last line above. We then define the property in the constructor (lines 6 through 8), again similar to what we saw in the previous tutorial. However there are some differences. First, our lambda function takes an argument name which is then used within the function itself. Second, there is an additional buttonEnabled argument. This argument needs to be an IObservable<bool> which says whether the command can execute or not. Essentially if the command can’t execute then the button will switch into a disabled state. Using ReactiveUI WhenAnyValue method on our class we can perform this check (see the ReactiveUI documentation for more examples) (lines 3 through 5). Lastly we had to specify the type of the argument into the function, in this case name which is a string. With that all configured it’s ready to go and the application is complete.



Picture of Me (Hank)

Categories

Updates (124)
Journal (115)
Software Engineering (91)
Daily Updates (84)
Commentary (66)
Methodology (57)

Archive

2019
2018
2017
2016
2015
2014
2013