0

I believe this is a quirk of how the ListView works. I have included a problem description what I think is going on, and my question: how do I get around it?

Problem Description

I have a ListView, with a ComboBox specified inside of the ItemTemplate. The ComboBox shows an editable property on the items displayed in the ListView.

Per the "tip" in the official documentation (under the heading "Item Selection"), I am handling the Loaded event to initialize the SelectedItem property in the ComboBox. (I am doing this because the data is being pulled from an asynchronous source and may not be available right away - I could not get data-binding in XAML alone to work).

When the page first loads, this works flawlessly. When I try to add items to the collection afterwards, however - I get some unexpected behavior... despite using the same function (SetItems() in the sample below) to do the updates both times. Using the print statement in the sample below, I have confirmed that when the page first loads, the output is as expected: each item in the list has an output line, and the SelectedValue in each ComboBox (which displays the Status property on each item in the example below) is correct.

When I update the list later (after the page and ListView initially load), however, by calling that same SetItem() function (perhaps after adding an item to the list), the output from that same print statement is NOT as expected. Only a single print statement appears, and prints a seemingly random item from the list, along with the incorrect Status. This is unexpected because I am clearing the ObservableCollection, and re-adding ALL items to it (including the new items).

In the page XAML:

<ListView ItemsSource="{x:Bind MyCollection}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="models:MyDataType">
            <!-- Some things omitted for brevity -->
            <ComboBox x:Name="MyComboBox"
                      Tag="{x:Bind ID}"
                      SelectedValuePath="Status"
                      Loaded="MyComboBox_Loaded"></ComboBox>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

The code behind with collection, event handler, and collection-altering code:

// The collection.
public ObservableCollection<MyDataType> MyCollection { get; set; }

// The ComboBox Loaded event handler.
private void MyComboBox_Loaded(object sender, RoutedEventArgs e)
{
    // Get the ID of the item this CombBox is for.
    ComboBox comboBox = sender as ComboBox;
    string id = comboBox.Tag as string;

    // Get a reference to the item in the collection.
    MyDataType item = this.MyCollection.Where(i => i.ID == id).FirstOrDefault();

    // Initialize the ComboBox based on the item's property.
    comboBox.SelectedValue = item.Status;

    // Wire up the Selection Changed event (doing this after initialization prevents the handler from running on initialization).
    comboBox.SelectionChanged += this.MyComboBox_SelectionChanged;

    // Print statement for debugging.
    System.Diagnostics.Debug.WriteLine("ComboBox for item " + item.ID + " loaded and set to " + item.Status);
}

// The function used to update the observable collection (and thus bound listview).
public void SetItems(List<MyDataType> items)
{
    // Clear the collection and add the given items.
    this.MyCollection.Clear();
    items.ForEach(i => this.MyCollection.Add(i));
}

What I think is going on

I believe this is a quirk of how the ListView works. I remember reading somewhere from past experience with item repeaters in a different part of .NET that the UI for each item may not actually be created new each time; UI items may be recycled and reused for efficiency reasons. I am going to guess what is happening here is similar; and the "reused" items are not firing the Loaded event, because they were never re-loaded.

So how do I get around it, and force the ComboBoxes to update each time the collection is changed, like how it does when the page first loads?

UPDATE

The unexpected output from the print statement does not appear to be random - it appears to be consistently the last item in the ListView. So whenever the SetItem() function is called (after the initial time when the page loads), ONLY the ComboBox for the last item in the ListView fires its Loaded event. The ComboBox on the other items are scrambled, however... almost like they were all shifted up by one (and so have the Status of the next item in the ListView rather than the item they are on).

2 Answers2

1

You don't need to use Loaded for this.

Let me show you a simple example:

MainPage.xaml

<Page
    x:Class="ListViewWithComboBoxExample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:ListViewWithComboBoxExample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:models="using:ListViewWithComboBoxExample.Models"
    x:Name="RootPage"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid RowDefinitions="Auto,*">
        <Button
            Grid.Row="0"
            Click="AddRandomItemsButton_Click"
            Content="Add random item" />
        <ListView
            Grid.Row="1"
            ItemsSource="{x:Bind MyCollection, Mode=OneWay}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="models:MyDataType">
                    <!--
                        Using 'Binding' with 'ElementName' and 'Path' is the trick here
                        to access code-behind properties from the DataTemplate.
                    -->
                    <ComboBox
                        ItemsSource="{Binding ElementName=RootPage, Path=MyComboBoxOptions}"
                        SelectedValue="{x:Bind Status, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                        Tag="{x:Bind ID, Mode=OneWay}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>

MainPage.xaml.cs

using ListViewWithComboBoxExample.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.ObjectModel;

namespace ListViewWithComboBoxExample;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
    }

    public ObservableCollection<string> MyComboBoxOptions { get; set; } = new()
    {
        "Active",
        "Inactive",
        "Unknown",
    };

    // ObservableCollections notify the UI when their contents change,
    // not when the collection itself changes.
    // You need to instantiate this here or in the constructor.
    // Otherwise, the binding will fail.
    public ObservableCollection<MyDataType> MyCollection { get; } = new();

    private void SetItem(MyDataType item)
    {
        MyCollection.Add(item);
    }

    private void AddRandomItemsButton_Click(object sender, RoutedEventArgs e)
    {
        MyCollection.Clear();

        Random random = new();
        int itemsCount = random.Next(100);

        for (int i = 0; i < itemsCount; i++)
        {
            MyDataType newItem = new()
            {
                ID = random.Next(100).ToString(),
                Status = MyComboBoxOptions[random.Next(MyComboBoxOptions.Count)],
            };

            SetItem(newItem);
        }
    }
}
Andrew KeepCoding
  • 7,040
  • 2
  • 14
  • 21
  • I tried implementing this, adapted to my code. It results in a null reference error from something that should never be null. The stack trace simply ends with [external code]. I am guessing the TwoWay binding mode is somehow trying to write a null value to something that otherwise is never null? For reference, MyDataType implements INotifyPropertyChanged, as does the Status (which is also a class). Would updating my question help? – Michael Kintscher they-them Jul 25 '23 at 21:20
  • If I toss in a null check where it throws the null reference error (and simply return), then the code runs, but the binding just doesn't work at all. I have verified that MyDataType and the Status property on MyDataType are never null nor set to null in my code. – Michael Kintscher they-them Jul 25 '23 at 21:22
  • I asked a separate question about the issue I am having with implementing this solution: https://stackoverflow.com/questions/76766943/how-to-twoway-data-bind-nested-inotifypropertychanged-viewmodels-to-combobox-ins. – Michael Kintscher they-them Jul 25 '23 at 22:44
  • 1
    Answered. Cheers. – Andrew KeepCoding Jul 26 '23 at 07:32
0

It sounds like you need to use the SelectionChanged event handler rather than loaded. If I’m not mistaken, you can use them both, but it isn’t necessary. You can use a line of code in you method for your page (it’s the one that has “this.Initialize();”) that will load whatever it is you want on startup.

I’m not 100% sure what your program does. Try adding the other event handler; there are a few that could apply.