Home > other >  How to bind to a model in ListView instead of property in WinUI3?
How to bind to a model in ListView instead of property in WinUI3?

Time:11-25

I am trying to directly bind to a model (similar to the situation here), instead of the property on that model, but it is not working. I previously asked a question about this, and the answer had worked in UWP. I am now working in WinUI3 (packaged UWP) and am not sure if the same solution applies.

I have a class:

public class Message
{
    public string Title { get; set; }
    public string Content { get; set; }
}

I have a UserControl that displays that class:

public sealed partial class MessageControl : UserControl
{
    /// <summary>
    /// The message to display.
    /// </summary>
    public Message Message { get; set; }

    public MessageControl()
    {
        this.InitializeComponent();
    }
}

With XAML:

<UserControl
    x:Class="MessageClient.Controls.EmailControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MessageClient.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <StackPanel>
            <!-- Displays the title of the message. -->
            <TextBlock Name="HeaderTextBlock"
                       Text="{x:Bind Message.Title, Mode=OneWay}"></TextBlock>
        
            <!-- Displays the content of the message. -->
            <TextBlock Name="ContentPreviewTextBlock"
                       Text="{x:Bind Message.Content, Mode=OneWay}"></TextBlock>
        </StackPanel>
    </Grid>
</UserControl>

Now, I have a Window I am displaying that control on, inside of a list view:

<Window
    x:Class="MessageClient.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:EmailClient"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:controls="using:MessageClient.Controls" xmlns:models="using:MessageClient.Models"
    mc:Ignorable="d">

    <Grid>
        <!-- The list of messages. -->
        <ListView x:Name="MessagesListView"
                  ItemsSource="{x:Bind Messages}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="models:Message">
                    <controls:MessageControl Margin="5"
                                           Message="{x:Bind Mode=OneWay}"></controls:MessageControl>

                    <!-- TEST 1 -->
                    <!--<TextBlock Text="{x:Bind Title}"></TextBlock>-->
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <!-- TEST 2 -->
        <!--<controls:MessageControl Message="{x:Bind TestMessage, Mode=OneWay}"></controls:MessageControl>-->
    </Grid>
</Window>

And the code behind for the Window is:

public sealed partial class MainWindow : Window
{
    public ObservableCollection<Message> Messages;
    public Message TestMessage;

    public MainWindow()
    {
        this.InitializeComponent();

        // Populate the test message.
        this.PopulateTestMessages();
    }

    private void PopulateTestMessages()
    {
        // The TestDataGenerator just returns a staticaly defined list of test messages.
        this.Messages = new ObservableCollection<Message>(TestDataGenerator.GetTestMessages(10));
        this.TestMessage = this.Messages.First();
    }
}

When I run this code (which compiles and runs), I get a window with the expected number of items in the ListView (10), but they are all blank. There are also no XAML binding failures being reported (via the new feature in Visual Studio 2022). In an attempt to investigate this, I added in a textblock inside of the listview (denoted as TEST 1 in the comments) bound to the Title property of the Message. This worked as expected, displaying the correct titles for the 10 messages. I then added a single MessageControl to the Window outside of the ListView (denoted as TEST 2 in the comments) bound to a single Message property on the Window. This also worked exactly as expected.

Am I missing something? Why does the specific case of binding to a model directly (as opposed to a property on that model - i.e. a binding with no path) not work, when the data is there and works with other binding configurations?

CodePudding user response:

Turns out this specific case exposes some of the ordering of how bindings work in XAML. To understand what is happening here:

  1. The Window is being initialized, which initializes its UI, including the ListView.
  2. The Message models are then retrieved and added to the ObservableCollection.
  3. When each Message is added to the ObservableCollection, the ListView using that ObservableCollection as its ItemsSource then creates a new Item using the ItemTemplate specified in the XAML, and then initializes and binds the newly initialized MessageControl to the Message. The TEST 1 verifies this is happening.

The problem here is in step 3: The MessageControls are initialized AND THEN bound to. When the binding happens, the UI is never notified that it needs to update the binding. This is why adding Mode=OneWay to the {x:Bind} doesn't fix the issue. The OneWay mode tells the binding to update whenever the model updates... but the XAML is still never aware the model was updated.

I am not entirely sure why this is different than the other two bindings (the TEST 1 and TEST 2 comments), but this seems to be an issue with user-defined types (classes) specifically, and NOT with primitive or built-in type properties (such as string and int).

The fix is to enable the model to inform (or "notify") the UI that is has been updated. This is done be implementing the INotifyPropertyChanged interface. Update the MessageControl to:

public sealed partial class MessageControl : UserControl, INotifyPropertyChanged
{
    private Message _message;
    /// <summary>
    /// The message to display.
    /// </summary>
    public Message Message
    {
        get { return this._message; }
        set
        {
            this._message = value;
            this.RaisePropertyChanged("Message");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    public EmailControl()
    {
        this.InitializeComponent();
    }
}

A final note: implementing INotifyPropertyChanged MUST be done on the MessageControl for this to work. Implementing INotifyPropertyChanged on the Message model would allow for the MessageControl (or any other data bound UI, such as the commented TEST 1) to update if a particular property of the Message model updates, but does not fix the issue of the Message model iself on the MessageControl being updated.

  • Related