Skip to content

10 Advanced Data Binding

Matthew Leibowitz edited this page Apr 16, 2019 · 5 revisions

In this section we will look at doing more complex data binding. We will look at passing parameters to commands, binding to elements in collections and binding to nested objects. We will also look at using converters to format the data from the view model into a format that will look best in the page.

You can view the code and the diff for this step on GitHub.

Command parameters

Our tic-tac-toe game is coming along nicely, but we are still missing the main part. The board. Even though it is on the screen and we can tap the squares, we still are not placing any marks.

If we were to try and implement the logic to place an X or an O on the board, we will find that we have no real idea what button was tapped. We need a way for the page to pass some additional information to the view model.

To do this, we can make use of parameters, or more specifically, command parameters. These are provided by the CommandParameter property of the buttons. This property can be any value we choose as it is just used by the command.

In our case, we can use the index of the button so we can know which button was tapped. So, for the top left button, we have a value of "0". For the top right button, we have a value of "2".

So, if we are to take the top left button, we will now have an additional CommandParameter property:

<Button Grid.Row="0" Grid.Column="0" d:Text="X"
        Command="{Binding MakeMoveCommand}"
        CommandParameter="0" />

We can do the same for all the buttons until we have nine buttons, each with a command parameter value of "0" to "8".

This is all we have to do to add parameters to commands in the XAML, but we need to update our command in the view model to receive these values. This is pretty simple to do, we need to use the generic Command<T> instead of the non-generic Command:

MakeMoveCommand = new Command<string>(OnMakeMove);

We use a T of string because the value that is coming from the XAML is actually string.

When we do this, we also have to update the OnMakeMove method to now have a parameter of string:

private void OnMakeMove(string indexString)
{
    // ...
}

If we run the game now with a breakpoint in this method, we will see that we get different index values depending on which button we tap.

Binding to nested or child objects

In our previous data bindings, we bound directly to a property on the view model. This is typically what we would do, but in some cases the object is a complex object or has complex properties.

If we go to the BoardView.xaml file, we can start to add more bindings. In our game, we will access the Board property, but we want to bind each button to a particular element in the array. This is actually quite simple to do, we just specify the index in the binding:

<Button Text="{Binding Board[0]}" />

The XAML engine will now bind to the first element in the Board property of the view model. This also works for all types of properties on the Board property. We could even bind to the Length property of the array:

<Button Text="{Binding Board.Length}" />

If the Length property was a bindable object, then any changes to that property would update the button's text.

In our game, we want to add a binding from the Text property of the buttons to each element in the array. We should now go through each button and add the binding, making sure to use the correct index for each button.

If we are using the previewer, we won't see any changes to the preview because the previewer does not know what type of view model we are using.

In the page, we specified our GameViewModel. If we do that in the view, we will override the object from the page and end up with multiple view models. So, in this case, we can actually use design-time data to specify the view model:

<d:ContentView.BindingContext>
    <local:GameViewModel />
</d:ContentView.BindingContext>

Value converters

When we run the game, we can see that there is now text on each of the buttons. This is not exactly what we want, but we are close.

To remove the values, and to use the correct symbol, we can use a value converter. Value converters are little pieces of logic that runs when the XAML engine wants to update the value of a control on the screen, or when it is going to send a value to the view model.

In our case, we want to convert the enum value from the "Nobody", "X" and "O" values to the correct string: "", "X" and "O". We essentially want to use an empty string when there is no player, and then the correct player's mark when there is.

Value converters are simple classes that implement the IValueConverter interface. We, we can add a new class to our project and add the interface:

public class PlayerConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
    }
}

This interface has two methods: Convert and ConvertBack. Convert is executed when the data is going from the source to the destination, and in our case, from the view model to the view. ConvertBack is in the opposite direction.

Both methods are useful if we have a two-way binding. But in our case, we just have a one-way binding so we aren't going to use the ConvertBack method and can just throw.

In the Convert method, we want to convert Player enum value to the string we want to see on the board. To do this, we convert the X and O values to strings, and the rest we just return an empty string. This way the board will be empty unless there is a specific mark there.

public class PlayerConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var player = (Player)value;
        if (player == Player.X)
            return "X";
        if (player == Player.O)
            return "O";
        return string.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Using value converters in XAML

To use our new value converter, we first need to add it to the XAML resources. This is so that we can re-use the same converter for all the buttons. We can either add it to the board view's resources, the page's resources or the app's resources. Since we might be using this in multiple pages, we will add it to the app's resources in App.xaml:

<local:PlayerConverter x:Key="PlayerConverter" />

The type name and the key do not have to be the same, but we are just using the same value in this case.

To actually use this in a binding, we can set the Converter property of the Binding markup extension. If we go back to the first button in our game board, we can see what the new binding will look like:

<Button Text="{Binding Board[0], Converter={StaticResource PlayerConverter}}" />

Just like control properties, we can use resources in our markup extension properties. This allows us to only have a single converter in memory, and all the buttons use the same one.

We can now add this converter to all the buttons. When we do that and run the game, we can see that the buttons are now all blank - which is what we want:

Updating the board array

At this point in our game, we can see that our commands are working and the player is changing. But, no marks are being placed on the board.

In order to have the board update, we just need to add a few lines of code to the OnMakeMove in the view model. We need to first parse the string and then make sure that is is a valid position. After that, we check that the board square is empty, and if it is, we can update the array. The last thing to do is to let the bindings know that the array has changed using the OnPropertyChanged method:

private void OnMakeMove(string indexString)
{
    if (State == GameState.GameOver)
        return;

    if (!int.TryParse(indexString, out var index))
        return;
    if (index < 0 || index >= Board.Length)
        return;

    if (Board[index] != Player.Nobody)
        return;

    Board[index] = CurrentPlayer;
    OnPropertyChanged(nameof(Board));

    CurrentPlayer = CurrentPlayer == Player.X
        ? Player.O
        : Player.X;
}

Even though we update the array Board[index] = CurrentPlayer, there is no way that the view will know as a simple array does not implement the INotifyPropertyChanged interface.

As a result, we have to manually invoke OnPropertyChanged. When we do so, we also have to pass in the name of the property that is updated. This is because the OnPropertyChanged method will try and determine what property called this method. It will find that it is a method named OnMakeMove which doesn't mean anything to the bindings. Thus, we have to explicitly set it.

We don't have to worry about CurrentPlayer as it has it's own setter which handles the bindings.

When we run our game, we can see that we can almost play tic-tac-toe - even if it can't detect wins or understand the game is over.

Clone this wiki locally