我相信这是
ListView
工作原理的一个怪癖。我已经包含了我认为正在发生的问题描述,以及我的问题:我如何解决它?
问题描述
我有一个
ListView
,在 ComboBox
内部指定了 ItemTemplate
。 ComboBox
显示 ListView
中显示的项目的可编辑属性。
根据官方文档中的“提示”(在“项目选择”标题下),我正在处理
Loaded
事件以初始化 SelectedItem
中的 ComboBox
属性。 (我这样做是因为数据是从异步源中提取的,并且可能无法立即可用 - 我无法单独在 XAML 中进行数据绑定工作)。
当页面首次加载时,效果完美。然而,当我随后尝试将项目添加到集合中时,我得到了一些意外的行为......尽管使用相同的函数(下面示例中的
SetItems()
)来进行两次更新。使用下面示例中的 print 语句,我已经确认,当页面首次加载时,输出符合预期:列表中的每个项目都有一个输出行,并且每个 SelectedValue
中的 ComboBox
(在下面的示例中显示每个项目的 Status
属性)是正确的。
当我稍后更新列表时(在页面和
ListView
初始加载之后),但是,通过调用相同的 SetItem()
函数(可能在将项目添加到列表之后),同一 print 语句的输出并不符合预期。仅出现一条打印语句,并打印列表中看似随机的项目以及不正确的 Status
。这是意外的,因为我正在清除 ObservableCollection
,并重新向其中添加所有项目(包括新项目)。
在页面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 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));
}
我认为发生了什么
我相信这是
ListView
工作原理的一个怪癖。我记得在 .NET 的不同部分中从过去使用项目重复器的经验中读到过,每个项目的 UI 实际上可能不会每次都创建新的;出于效率原因,UI 项目可能会被回收和重复使用。我猜这里发生的事情是相似的;并且“重复使用”的项目不会触发 Loaded
事件,因为它们从未重新加载。
那么我该如何解决这个问题,并强制 ComboBox 在每次集合更改时更新,就像页面首次加载时的情况一样?
更新
打印语句的意外输出似乎不是随机的 - 它似乎始终是
ListView
中的最后一项。因此,每当调用 SetItem()
函数时(在页面加载的初始时间之后),只有 ComboBox
中最后一项的 ListView
才会触发其 Loaded
事件。其他项目上的 ComboBox
被打乱了,但是......几乎就像它们都向上移动了一个(因此 Status
中下一个项目的 ListView
而不是它们所在的项目)。
听起来您需要使用 SelectionChanged 事件处理程序而不是加载。如果我没记错的话,你可以同时使用它们,但这不是必需的。您可以在页面的方法中使用一行代码(即具有“this.Initialize();”的代码),它将在启动时加载您想要的任何内容。
我不能 100% 确定你的程序是做什么的。尝试添加其他事件处理程序;有一些可以申请。
您不需要为此使用
Loaded
。
让我向您展示一个简单的例子:
主页.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);
}
}
}