-1

I have a datatype (model) I would like to display the data for in my UI by showing several properties using data binding.. It works in a GridView or ListView, but how do I do this when I only want a single model bound instead of a collection?

To do this with a collection, the following works in a ListView:

<ListView x:Name="MyListView"
          ItemsSource="{x:Bind Shapes, Mode=OneWay}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="models:Shape">
            <StackPanel>
                <TextBlock Text="{x:Bind Name}"></TextBlock>
                <TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
                <TextBlock Text="{x:Bind Color}"></TextBlock>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

On a page with a ObservableCollection of type Shape called Shapes:

public sealed partial class MyPage : Page
{
    // ...
    public ObservableCollection<Shape> Shapes { get; set; }
    // ...
}

With the following model Shape:

public class Shape
{
    public string Name { get; set; }
    public string NumberOfSides { get; set; }
    public string Color { get; set; }
}

I want to do something like this, but this does not work:

<Grid>
    <StackPanel>
        <TextBlock Text="{x:Bind Name}"></TextBlock>
        <TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
        <TextBlock Text="{x:Bind Color}"></TextBlock>
    </StackPanel>
</Grid>

1 Answers1

0

The data-binding is actually being done to the ListView, and the DataTemplate is simply declaring the layout to display the bound model with.

To accomplish this with a single bound item instead of a collection, you need to use a control that still has a template property. This is where the ContentControl comes in (Microsoft's official documentation). The ContentControl has a ContentTemplate property, which can contain a DataTemplate the same way a ListView or GridView can! You can then set the Content property of the ContentControl in the C# code, or bind to it (the same way you would bind to an ItemsSource property of a ListView or GridView, only with a single item instead of a collection).

The Simple Way

The following example works (Note that the DataTemplate and all of it's children are identical to how they would appear in a ListView or GridView):

<ContentControl x:Name="MyContentControl">
    <ContentControl.ContentTemplate>
        <DataTemplate x:DataType="models:Shape">
            <StackPanel>
                <TextBlock Text="{x:Bind Name}"></TextBlock>
                <TextBlock Text="{x:Bind NumberOfSides}"></TextBlock>
                <TextBlock Text="{x:Bind Color}"></TextBlock>
            </StackPanel>
        </DataTemplate>
    <ContentControl.ContentTemplate>
</ContentControl>

Then in your C# code:

public sealed partial class MyPage : Page
{
    // ...
    public void SetShape(Shape shape)
    {
        this.MyContentControl.Content = shape;
    }
    // ...
}

The FULL Data Binding Way

You can also use data binding to bind to the shape property, but this will require some more work. Start by adding the binding to the ContentControl as follows:

<ContentControl x:Name="MyContentControl"
                Content="{x:Bind MyShape}">
    <ContentControl.ContentTemplate>
         <!-- Contents all the same as before -->
    <ContentControl.ContentTemplate>
</ContentControl>

And add the MyShape property to bind to on MyPage:

public sealed partial class MyPage : Page
{
    // ...
    public Shape MyShape { get; set; }
    // ...
}

As is, this will not work. It may work when you set it initially, but if you change MyShape, the bound UI will not update.

Notice that if you were using ObservableCollection (such as in the ListView example), you can get the UI to update when you call Add() or Remove() functions of the ObservableCollection, but not when you change the ObservableCollection reference itself. The reason is that the ObservableCollection implements INotifyPropertyChanged which is what tells the bindings to update when you change the set of items in the collection. The following will not automatically work:

public sealed partial class MyPage : Page
{
    // ...
    public Shape MyShape { get; set; }
    // ...
    public void UpdateShape(Shape newShape)
    {
        this.MyShape = newShape;
    }
}

To get this to work, you need to implement INotifyPropertyChanged on MyPage. This requires three steps (which may sound intimidating, but work the same way for any property):

  1. Implement the interface INotifyPropertyChanged.
  2. Add the PropertyChanged event.
  3. Modify the MyShape setter to raise the PropertyChanged event.

Implement the interface INotifyPropertyChanged.

public sealed partial class MyPage : Page, INotifyPropertyChanged
{
    // ...
}

Add the PropertyChanged event.

public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raise the PropertChanged event for the given property name.
/// </summary>
/// <param name="name">Name of the property changed.</param>
public void RaisePropertyChanged(string name)
{
    // Ensure a handler is listening for the event.
    if (this.PropertyChanged != null)
    {
        this.PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

Modify the MyShape setter to raise the PropertyChanged event.

private Shape myShape;
public Shape MyShape
{
    get => this.myShape;
    set
    {
        this.myShape = value;
        this.RaisePropertyChanged("MyShape");
    }
}

Your final C# code will look like this:

public sealed partial class MyPage : Page, INotifyPropertyChanged
{
    // ...

    private Shape myShape;
    public Shape MyShape
    {
        get => this.myShape;
        set
        {
            this.myShape = value;
            this.RaisePropertyChanged("MyShape");
        }
    }

    // ...

    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Raise the PropertChanged event for the given property name.
    /// </summary>
    /// <param name="name">Name of the property changed.</param>
    public void RaisePropertyChanged(string name)
    {
        // Ensure a handler is listening for the event.
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    // ...

    public void UpdateShape(Shape newShape)
    {
        this.MyShape = newShape;
    }
}

NOW your ContentControl will work as expected with the different BindingMode values (OneTime, OneWay, and TwoWay).

If you want your bound controls WITHIN the ContentControl to update when you change a property of the shape, such as have the <TextBlock Text="{x:Bind Name}"> update when you do:

this.MyShape.Name = "A New Name";

You can similarly implement INotifyPropertyChanged on your Shape class itself with the same basic steps. This is the same whether you are using a ContentControl, GridView, ListView, or any other data-bound control. Basically, each layer you want to be able to update the properties of, and have a data bound UI update, you need to do this. This also needs to be done regardless of which of the two ways you used from this answer. You can refer to my answer here for details on this.