NoesisGUI

Data Binding

github Tutorial Data

NoesisGUI provides a simple and powerful way to auto-update data between the business model and the user interface. This mechanism is called Data Binding. They key to data binding is a Binding object that "glues" two properties together and keeps a channel of communication open between them. You can set a Binding once, and then have it do all the synchronization work for the remainder of the application's lifetime.

In this tutorial we will explain the different ways you might want to use data binding.

Using Binding in XAML

DataBindingTutorialImg1.jpg

To use Binding in XAML, you directly set the target property to a Binding instance and then use the standard markup extension syntax to set its properties. The following example shows a simple binding between the text of a TextBox and a Label that reflects the typed value:

<StackPanel
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    VerticalAlignment="Center">

    <TextBox x:Name="textbox" />
    <Label Content="{Binding Text, ElementName=textbox}" />

</StackPanel>

NOTE

The source of a data binding can be a normal property or a DependencyProperty. The target property of the binding must be a DependencyProperty.

Data Context

It is quite common for many elements in the same UI to bind the same source object. For this reason, noesisGUI supports specifying an implicit data source rather than explicitly marking every Binding with Source, RelativeSource or ElementName. This implicit data source is also known as a data context.

To designate a source object as a data context, simply find a common parent element and set its DataContext property to the source object. When encountering Binding without an explicit source object, noesisGUI traverses up the logical tree until it finds a non null DataContext. Here is an example of setting a data context that will be used for all of the examples to come:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <UserControl.Resources>
        <DataModel x:Name="dataModel" />
    </UserControl.Resources>

</UserControl>

Binding to Plain Properties

NoesisGUI supports any normal property on any object as a data-binding source. For example, the following XAML binds several properties that belong to instances found in the current data context:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <UserControl.Resources>
        <DataModel1 x:Name="dataModel">
            <DataModel1.Person>
                <Person Weight="90">
                    <Person.Name>
                        <Name First="John" Last="Doe" />
                    </Person.Name>
                </Person>
            </DataModel1.Person>
        </DataModel1>
    </UserControl.Resources>

    <StackPanel DataContext="{StaticResource dataModel}" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock Text="{Binding Person.Name.First}" />
        <TextBlock Text="{Binding Person.Name.Last}" />
        <TextBlock Text="{Binding Person.Weight}" />
    </StackPanel>

</UserControl>

There is a big caveat to using a plain property as a data-binding source, however. Because such properties have no automatic plumbing for change notification, the target is not kept up to date as the source property value changes without doing a little extra work. To keep the target and source properties synchronized, the source object must implement the INotifyPropertyChanged interface.

C++

There are a few important details you must pay attention to whenever you implement a data model in C++:

  • As said, the interface INotifyPropertyChanged must be implemented
  • Expose properties using reflection macros macros with getter and setter functions
  • In each setter, check if the property changed event must be fired
  • Implement serialization functions
class Name: public BaseComponent, public INotifyPropertyChanged
{
public:
    const NsChar* GetFirst() const
    {
        return _first.c_str();
    }

    void SetFirst(const NsChar* first)
    {
        if (_first != first)
        {
            _first = first;
            _propertyChanged(this, NSS(First));
        }
    }

    const NsChar* GetLast() const
    {
        return _last.c_str();
    }

    void SetLast(const NsChar* last)
    {
        if (_last != last)
        {
            _last = last;
            _propertyChanged(this, NSS(Last));
        }
    }

    PropertyChangedEventHandler& PropertyChanged()
    {
        return _propertyChanged;
    }

    void Serialize(SerializationData* data) const
    {
        data->Serialize("First", _first);
        data->Serialize("Last", _last);
    }

    void Unserialize(UnserializationData* data, NsUInt32 version)
    {
        data->Unserialize("First", _first);
        data->Unserialize("Last", _last);
    }

private:
    NsString _first;
    NsString _last;

    PropertyChangedEventHandler _propertyChanged;

    NS_IMPLEMENT_INLINE_REFLECTION(Name, BaseComponent)
    {
        NsMeta<TypeId>("Name");
        NsImpl<INotifyPropertyChanged>();
        NsProp("First", &Name::GetFirst, &Name::SetFirst);
        NsProp("Last", &Name::GetLast, &Name::SetLast);
    }
};

class Person: public BaseComponent, public INotifyPropertyChanged
{
public:
    Name* GetName() const
    {
        return _name.GetPtr();
    }

    void SetName(Name* name)
    {
        if (_name != name)
        {
            _name.Reset(name);
            _propertyChanged(this, NSS(Name));
        }
    }

    NsFloat32 GetWeight() const
    {
        return _weight;
    }

    void SetWeight(const NsFloat32 weight)
    {
        if (_weight != weight)
        {
            _weight = weight;
             _propertyChanged(this, NSS(Weight));
        }
    }

    PropertyChangedEventHandler& PropertyChanged()
    {
        return _propertyChanged;
    }

    void Serialize(SerializationData* data) const
    {
        data->Serialize("Name", _name);
        data->Serialize("Weight", _weight);
    }

    void Unserialize(UnserializationData* data, NsUInt32 version)
    {
        data->Unserialize("Name", _name);
        data->Unserialize("Weight", _weight);
    }

private:
    Ptr<Name> _name;
    NsFloat32 _weight;

    PropertyChangedEventHandler _propertyChanged;

    NS_IMPLEMENT_INLINE_REFLECTION(Person, BaseComponent)
    {
        NsMeta<TypeId>("Person");
        NsImpl<INotifyPropertyChanged>();
        NsProp("Name", &Person::GetName, &Person::SetName);
        NsProp("Weight", &Person::GetWeight, &Person::SetWeight);
    }
};

class DataModel1: public BaseComponent, public INotifyPropertyChanged
{
public:
    Person* GetPerson() const
    {
        return _person.GetPtr();
    }

    void SetPerson(Person* person)
    {
        if (_person != person)
        {
            _person.Reset(person);
            _propertyChanged(this, NSS(Person));
        }
    }

    PropertyChangedEventHandler& PropertyChanged()
    {
        return _propertyChanged;
    }

    void Serialize(Noesis::Core::SerializationData* data) const
    {
        data->Serialize("Person", _person);
    }

    void Unserialize(Noesis::Core::UnserializationData* data, NsUInt32 version)
    {
        data->Unserialize("Person", _person);
    }

private:
    Ptr<Person> _person;
    PropertyChangedEventHandler _propertyChanged;

    NS_IMPLEMENT_INLINE_REFLECTION(DataModel1, BaseComponent)
    {
        NsMeta<Noesis::Core::TypeId>("DataModel1");
        NsImpl<INotifyPropertyChanged>();
        NsProp("Person", &DataModel1::GetPerson, &DataModel1::SetPerson);
    }
};

C#

Things are a lot easier in C#. You just basically need to implement the INotifyPropertyChanged interface.

public class Name: INotifyPropertyChanged
{
    private string _first;
    public string First
    {
        get { return _first; }
        set
        {
            if (_first != value)
            {
                _first = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("First"));
                }
            }
        }
    }

    private string _last;
    public string Last
    {
        get { return _last; }
        set
        {
            if (_last != value)
            {
                _last = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Last"));
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class Person: INotifyPropertyChanged
{
    private Name _name;
    public Name Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Name"));
                }
            }
        }
    }

    private float _weight;
    public float Weight
    {
        get { return _weight; }
        set
        {
            if (_weight != value)
            {
                _weight = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Weight"));
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class DataModel1
{
    private Person _person;
    public Person Person
    {
        get { return _person; }
        set
        {
            if (_person != value)
            {
                _person = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Person"));
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Indexed Properties

In C#, Indexed Properties can be used as source of a data-binding. That way, you can write something like this:

class Person
{
    public string Name {get; set;}
    public string PhoneNumber {get; set;}
}

class Contacts
{
    public IEnumerable<Person> Persons {get; set;}

    public Person this[string Name]
    {
        get
        {
            return Persons.Where(p => p.Name == Name).FirstOrDefault();
        }
    }
}
<TextBox Text="{Binding Contacts[John].PhoneNumber}" />

NOTE

Only indexers with the key type being int or string are supported in noesisGUI.

StringFormat

If you need to format the text display of a given value, you can do so using the StringFormat attribute in the binding declaration. With StringFormat you can format the output of information without using code behind or value converters.

 <UserControl
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

     <UserControl.Resources>
         <DataModel1 x:Name="dataModel">
             <DataModel1.Person>
                 <Person Weight="90">
                     <Person.Name>
                         <Name First="John" Last="Doe" />
                     </Person.Name>
                 </Person>
             </DataModel1.Person>
         </DataModel1>
     </UserControl.Resources>

     <StackPanel DataContext="{StaticResource dataModel}" HorizontalAlignment="Center" VerticalAlignment="Center">
         <TextBlock Text="{Binding Person.Name.First}" />
         <TextBlock Text="{Binding Person.Name.Last}" />
         <TextBlock Text="{Binding Person.Weight, StringFormat=Weight is {0:F2}}" />
     </StackPanel>

 </UserControl>

More examples of StringFormat:

<TextBlock Text="{Binding Amount, StringFormat={}{0:D}}" />
<TextBlock Text="{Binding Amount, StringFormat=Total: {0:D}}" />
<TextBlock Text="{Binding Amount, StringFormat=Total: {0:D} units}" />

Notice the "{}"" just after the StringFormat attribute? What that is doing is escaping the text after the "="" sign. You need to do this because we do not have any text directly after the "="" sign.

Binding Dependency Properties

Dependency properties have plumbing for change notifications built in. This facility is the key to noesisGUI's ability to keep the target property and source property in sync. You don't need to use INotifyPropertyChange when you implement Dependency Properties. In fact, this should be the preferred method for creating bindable properties if you are deriving from DependencyObject.

Dependency Properties are created imperatively in the code behind of the visual element, for example:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="View2"
    x:Name="root">

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBox Text="{Binding ElementName=root, Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Text="{Binding ElementName=root, Path=Text}" Width="100" />
    </StackPanel>

</UserControl>

As you can see we are setting the Mode property of the Binding. It can be set to one of the following values of the BindingMode enumeration:

DataBindingTutorialImg2.jpg
  • OneWay: The target is updated whenever the source changes.
  • TwoWay: A change to either the target or source updates the other.
  • OneWayToSource: The opposite of OneWay. The source is updated whenever the target changes.
  • OneTime: This works just like OneWay, except changes to the source are not reflected at the target. The target retains a snapshot of the source at the time the Binding is initiated.

TwoWay binding is appropriate for data-bounds forms, where you might have TextBoxes that get filled with data that the user is allowed to change. In fact, whereas most dependency properties default to OneWay binding, dependency properties such as TextBox.Text default to TwoWay binding.

We are also using the UpdateSourceTrigger property. When using TwoWay or OneWayToSource binding, you might want different behaviors for when and how the source gets updated. For example, if a user types in a TwoWay data-bound TextBox, do you want the source to be updated with each keystroke, or only when the user is done typing? Binding enables you to control such behavior with its UpdateSourceTrigger property.

UpdateSourceTrigger can be set to a member of the UpdateSourceTrigger enumeration, which has the following values:

  • PropertyChanged: The source is updated whenever the target property value changes.
  • LostFocus: When the target property value changes, the source is only updated after the target element loses focus.
  • Explicit: The source is only updated when you make an explicit call to BindingExpression.UpdateSource.

Just as different properties have different default Mode settings, they also have different default UpdateSourceTrigger settings. TextBox.Text defaults to LostFocus.

C++

class View2: public UserControl
{
public:
    const NsChar* GetText() const
    {
        return GetValue<NsString>(TextProperty).c_str();
    }

    void SetText(const NsChar* text)
    {
         SetValue<NsString>(TextProperty, text);
    }

    static const DependencyProperty* TextProperty;

private:
    NS_IMPLEMENT_INLINE_REFLECTION(View2, UserControl)
    {
        NsMeta<TypeId>("View2");

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsString>(TextProperty, "Text",
            FrameworkPropertyMetadata::Create(NsString(""), FrameworkOptions_None));
    }
};

C#

class View2: UserControl
{
    public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
        typeof(View2), new PropertyMetadata(""));

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
}

Binding to a Collection

So far we've only discussed binding to single objects, however, binding to a data collection is a common scenario. For example, a common scenario is to use an ItemsControl such as a ListBox, ListView, or TreeView to display a data collection.

It would make sense to create a Binding with ListBox.Items as the target property, but, alas, Items is not a dependency property. But ListBox (and all other ItemsControl) have an ItemsSource dependency property that exist specifically for this data-binding scenario.

DataBindingTutorialImg3.jpg
<ListBox ItemsSource="{Binding Source={StaticResource items}}" />

For the target property to stay updated with changes to the source collection, the source collection must implement an interface called INotifyCollectionChanged. Fortunately, noesisGUI already has a built-in class that does this work for you. It is called ObservableCollection.

<Grid
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="600">

    <Grid.Resources>
        <DataModel3 x:Name="dataModel" />

        <DataTemplate x:Key="TaskTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="100"/>
                    <ColumnDefinition Width="200"/>
                    <ColumnDefinition Width="200"/>
                </Grid.ColumnDefinitions>

                <Rectangle Width="15" Height="10" Fill="{Binding Color}" Stroke="Transparent" StrokeThickness="0"
                    Margin="5, 0, 5, 0" Grid.Column="0"/>
                <TextBlock Text="{Binding Name}" Margin="5, 0, 5, 0" Grid.Column="1"/>
                <TextBlock Text="{Binding Scale, StringFormat=P0}" Margin="5, 0, 5, 0" Grid.Column="2"/>
                <TextBlock Text="{Binding Pos}" Margin="5, 0, 5, 0" Grid.Column="3"/>
            </Grid>
        </DataTemplate>
    </Grid.Resources>

    <ListBox Height="100" DataContext="{StaticResource dataModel}"
        ItemsSource="{Binding Players}"
        ItemTemplate="{StaticResource TaskTemplate}" />

</Grid>

Note that we are using the ItemTemplate property to control how each item is rendered. This property is set to an instance of a DataTemplate. DataTemplate derives from FrameworkTemplate. Therefore, it has a VisualTree content property that can be set to an arbitrary tree of FrameworkElements.

NOTE

For very simple cases you can use the DisplayMemberPath property present on all ItemsControls. This property works hand in hand with ItemsSource. If you set it to an appropriate property path, the corresponding property value gets rendered for each item.

When you apply a data template, it is implicitly given an appropriate data context. When applied as an ItemTemplate, the data context is implicitly the current item in ItemsSource.

DataBindingTutorialImg6.jpg

C++

class Player: public BaseComponent
{
public:
    Player() {}
    Player(NsString name, Color color, NsFloat32 scale, NsString pos) : _name(name), _scale(scale),
        _pos(pos), _color(*new SolidColorBrush(color)) {}

private:
    NsString _name;
    NsFloat32 _scale;
    NsString _pos;
    Ptr<Brush> _color;

    NS_IMPLEMENT_INLINE_REFLECTION(Player, BaseComponent)
    {
        NsMeta<TypeId>("Player");
        NsProp("Name", &Player::_name);
        NsProp("Scale", &Player::_scale);
        NsProp("Pos", &Player::_pos);
        NsProp("Color", &Player::_color);
    }

};

class DataModel3: public BaseComponent
{
public:
    DataModel3()
    {
        _players = *new ObservableCollection<Player>;

        Ptr<Player> player0 = *new Player("Player0", Color::Red, 1.0f, "(0,0,0)");
        _players->Add(player0.GetPtr());

        Ptr<Player> player1 = *new Player("Player1", Color::Gray, 0.75f, "(0,30,0)");
        _players->Add(player1.GetPtr());

        Ptr<Player> player2 = *new Player("Player2", Color::Orange, 0.50f, "(0,-10,0)");
        _players->Add(player2.GetPtr());

        Ptr<Player> player3 = *new Player("Player3", Color::Green, 0.85f, "(0,-10,0)");
        _players->Add(player3.GetPtr());
    }

private:
    Ptr< ObservableCollection<Player> > _players;

    NS_IMPLEMENT_INLINE_REFLECTION(DataModel3, BaseComponent)
    {
        NsMeta<TypeId>("DataModel3");
        NsProp("Players", &DataModel3::_players);
    }
};

C#

public class Player
{
    public Player(string name, Color color, float scale, string pos)
    {
        Name = name;
        Color = new SolidColorBrush(color);
        Scale = scale;
        Pos = pos;
    }

    public string Name { get; private set; }
    public Brush Color { get; private set; }
    public float Scale { get; private set; }
    public string Pos { get; private set; }

}

public class DataModel3
{
    public DataModel3()
    {
        Players = new ObservableCollection<Player>();
        Players.Add(new Player("Player0", Colors.Red, 1.0f, "(0,0,0)"));
        Players.Add(new Player("Player1", Colors.Gray, 0.75f, "(0,30,0)"));
        Players.Add(new Player("Player2", Colors.Orange, 0.50f, "(0,-10,0)"));
        Players.Add(new Player("Player3", Colors.Green, 0.85f, "(0,-10,0)"));
    }

    public ObservableCollection<Player> Players { get; private set; }
}

A special subclasss of DataTemplate exists for working with hierarchical data. This class is called HierarchicalDataTemplate. It not only enables you to change the presentation of such data, but enables you to directly bind a hierarchy of objects to an element that intrinsically understands hierarchies, such as a TreeView or Menu control.

The idea is to use HierarchicalDataTemplate for every data typein the hierarchy but then use a simple DataTemplate for any leaf nodes. Each data template gives you the option to customize the rendering of the data type, but HierarchicalDataTemplate also enables you to specify its children in the hierarchy by setting its ItemsSource propery.

DataBindingTutorialImg7.jpg
<Grid
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid.Resources>
        <LeagueList x:Key="items" />
        <HierarchicalDataTemplate DataType="League" ItemsSource="{Binding Path=Divisions}">
            <TextBlock Foreground="LightCyan" Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate DataType="Division" ItemsSource="{Binding Path=Teams}">
            <TextBlock Foreground="Snow" Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
        <DataTemplate DataType="Team">
            <TextBlock Foreground="Moccasin" Text="{Binding Path=Name}"/>
        </DataTemplate>
    </Grid.Resources>

    <TreeView DataContext="{StaticResource items}">
        <TreeViewItem ItemsSource="{Binding Leagues}" Header="My Soccer Leagues" />
    </TreeView>
</Grid>

Value Converters

Whereas data templates can customize they way certain target values are rendered, value converters can morph the source value into a completely different target value. They enable you to plug in custom logic without giving up the benefits of data binding.

Value converters are often used to reconcile a source and target that are different data types. For example, you could change the background or foreground color of an element on the value of some non-Brush data source. Or, you could use it to simply enhance the information being displayed without the need for separate elements, such as adding an "item(s)" suffix to a raw count.

DataBindingTutorialImg4.jpg

For example, the following XAML toggles the visibility of an element based on the IsChecked property of a CheckBox using a converter named BooleanToVisibilityConverter.

<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Background="Gray">

    <Grid.Resources>
        <BooleanToVisibilityConverter x:Key="converter"/>
    </Grid.Resources>

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <CheckBox x:Name="checkBox" Content="Show Button" Width="100"/>
        <Button Content="Click" Margin="5" Visibility="{Binding ElementName=checkBox, Path=IsChecked,
            Converter={StaticResource converter}}"/>
    </StackPanel>

</Grid>

You can create your own converters by implementing the IValueConverter interface. An example is shown in the Extending Tutorial.

Solar System Example

DataBindingTutorialImg5.jpg

This example summarizes all the concepts presented at this tutorial. It basically consists of a ListBox. bound to an ObservableCollection of SolarSystemObject items.

<ListBox ItemsSource="{Binding Source={StaticResource solarSystem}, Path=SolarSystemObjects}" Focusable="False" />

The look of each planet is defined by a DataTemplate that uses data binding and a converter to generate the final look.

The full project is included with the rest of samples corresponding to this tutorial.

NOTE

This sample was adapted from the sample The power of Styles and Templates in WPF

© 2017 Noesis Technologies