Table of Contents

Bindings and commands

A view reads its values from its view-model, and runs its commands there. The link is made by name: an attribute written as {Binding Name} reads the view-model property called Name, and a button's Command names an ICommand property.

Binding a value

Write {Binding Name} as the value of an attribute:

<TextBox Id="name" Size="{240,30}" Text="{Binding PlayerName}" />
<CheckBox Id="remember" Text="Remember me" Checked="{Binding RememberMe}" />
<Label Id="gold" Size="{160,24}" Text="{Binding GoldText}" Visible="{Binding ShowGold}" />

These attributes accept a binding:

Control Attribute Direction
Label, Button Text View-model to view.
TextBox, PasswordBox Text Both ways: typing writes the property.
CheckBox Checked Both ways.
RadioButton Checked Both ways.
Slider Value Both ways.
ProgressBar Value View-model to view.
ComboBox SelectedKey Both ways.
ComboBox OptionsSource View-model to view.
Repeater ItemsSource View-model to view.
Repeater SelectedItem Both ways.
Image Source View-model to view.
Any control or layout Visible, Disabled View-model to view.

A CheckBox or RadioButton Text binds view-model to view, as a Label's does.

A literal can be used where a binding can, and it is then fixed when the view is built.

In C#, the same bindings are set by the Bind... methods, which take the property name: BindText, BindVisibility, BindDisabled, BindHoverKey, BindChecked, BindValue, BindSelectedKey, BindOptions, BindSource, BindItems and BindSelectedItem. BindVisibility("ShowGold") is Visible="{Binding ShowGold}".

Property types

A bound property should have the type the control expects: a bool for Visible, Disabled and Checked, a number for Value, a string for Text and SelectedKey, and an IEnumerable<KeyValuePair<string, string>> for OptionsSource.

When the value is not of the right type, the view converts it if it can, such as a number to text or a string to a number. A value that cannot be converted is passed on unchanged, and setting it on the control then fails.

A two-way binding writes to the property, so the property needs a public setter. A property with no setter is still read, but what the player does does not reach it.

Commands

A Button runs a command with Command. Write the name of an ICommand property, without braces; {Binding Name} is not read there:

<Button Id="play" Text="Play" Size="{240,40}" Command="PlayCommand" />
public ICommand PlayCommand { get; }

PlayCommand is set in the constructor. The usual type is DelegateCommand, which runs an action:

using BSGameLib.UI;

public class MenuViewModel : ViewModel
{
  public MenuViewModel(IViewManager viewManager) : base(viewManager)
  {
    PlayCommand = new DelegateCommand(() => ViewManager.SetView<GameViewModel>());
    QuitCommand = new DelegateCommand(() => { /* exit the game */ });
  }

  public ICommand PlayCommand { get; }

  public ICommand QuitCommand { get; }
}

public class GameViewModel : ViewModel
{
  public GameViewModel(IViewManager viewManager) : base(viewManager) { }
}

For a command that needs more than an action, derive from CommandBase and override Execute.

A Repeater has ActivateCommand, which runs when a row is double-clicked. It takes the same kind of name. See Controls.

Commands run from the game's input, on the game thread. A Command that names no property, or a property that is not an ICommand, does nothing when the button is pressed.

Telling the view about a change

Derive the view-model from ViewModelBase (which ViewModel already is), and set properties with Set. Set writes the field, and raises PropertyChanged only when the value is different:

using BSGameLib.UI;

public class ShopViewModel : ViewModelBase
{
  private string _goldText = string.Empty;

  public string GoldText
  {
    get => _goldText;
    set => Set(ref _goldText, value);
  }
}

When the view-model raises a change for a property, the view reads again only the values bound to that property. A value derived from several fields should be a property of its own that raises a change for its name. To read every bound value again, raise a change with no name: OnPropertyChanged(null). OnPropertyChanged() with no argument names the calling member, so from a method that is not a property it names something nothing is bound to.

A view-model that does not raise changes is read once, when its view is built. Its bound values are then fixed until the view is built again.

Lists

A Repeater draws one row per item in its ItemsSource. Each row's bindings read that item's properties, so an item's own Name is {Binding Name}. SelectedItem names a property that receives the selected item:

using BSGameLib.UI;
using BSGameLib.UI.Builder;

public class InventoryViewModel : ViewModel
{
  private ItemViewModel _selected;

  public InventoryViewModel(IViewManager viewManager) : base(viewManager) { }

  public IReadOnlyList<ItemViewModel> Items { get; private set; } = [];

  public ItemViewModel Selected
  {
    get => _selected;
    set => Set(ref _selected, value);
  }

  public void SetItems(IEnumerable<ItemViewModel> items)
  {
    // A new list, so that the repeater rebuilds its rows.
    Items = items.ToList();
    OnPropertyChanged(nameof(Items));
  }

  protected override void BuildView(IViewBuilder builder) =>
    builder.PopulateFromFile(Path.Combine(AppContext.BaseDirectory, "Views", "Inventory.view.xml"));
}

public class ItemViewModel : ViewModelBase
{
  private int _count;

  public string Name { get; init; } = string.Empty;

  public int Count
  {
    get => _count;
    set => Set(ref _count, value);
  }
}
<Repeater Id="items" Size="{300,240}" ItemsSource="{Binding Items}" SelectedItem="{Binding Selected}">
  <ItemTemplate>
    <StackLayout Orientation="Horizontal">
      <Label Size="{300,24}" Text="{Binding Name}" />
    </StackLayout>
  </ItemTemplate>
</Repeater>

A repeater keeps the same rows while it is given the same collection, so changing the contents of a list in place does not show. Give it a new collection. A row whose item raises PropertyChanged updates by itself.

Localized text

{Localize key} shows the text for a key in the current language:

<Label Id="title" Size="{240,32}" Text="{Localize menu.title}" />

The key is looked up when the view is built, so a view shows the language that was current at that time. To show a different language, build the views again with IViewManager.ReloadActiveView. Text that changes while a view is open should come from a view-model property and a binding instead. For the language setup, see Localization.

Resource references

{Resource id} uses a named colour, font or gradient from the view's resources or its templates:

<Label Id="status" Size="{240,24}" Color="{Resource theme.text}" />

Resources are read once, when the view is built. See Styles and themes.

When a name does not match

A binding to a property the view-model does not have, or to one whose value can't be converted to what the control takes, logs a warning, once for that binding, and is skipped. The control keeps its literal value, or its default.

Check the names against the view-model when the view is first opened. The schema check in Views and view-models does not see bindings.