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.