使用基本抽象 class 时,带有 ObservableCollection 的 UWP DependencyProperty 不会更新 UI
UWP DependencyProperty with ObservableCollection doesn't update UI when working with base abstract class
我遇到了一些与 DependencyProperty
一起使用的 ObservableCollection
的奇怪行为。
我在这里创建了最小的可重现场景:https://github.com/aosyatnik/UWP_ObservableCollection_Issue.
有 2 个问题,我看到但无法解释。
这是我的 MainViewModel
:
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace UWP_ObservableCollection
{
public class MainViewModel : BaseViewModel
{
public IList<ItemViewModel> ItemsAsList { get; private set; }
public ObservableCollection<ItemViewModel> ItemsAsObservableCollection { get; private set; }
public IList<ItemViewModel> ItemsRecreatedList { get; private set; }
public MainViewModel()
{
ItemsAsList = new List<ItemViewModel>();
ItemsAsObservableCollection = new ObservableCollection<ItemViewModel>();
ItemsRecreatedList = new List<ItemViewModel>();
}
public void AddNewItem()
{
var newItem = new ItemViewModel();
// First try: add to list and raise property change - doesn't work.
ItemsAsList.Add(newItem);
RaisePropertyChanged(nameof(ItemsAsList));
// Second try: with ObservableCollection - doesn't work?
ItemsAsObservableCollection.Add(newItem);
// Third try: recreate the whole collection - works
ItemsRecreatedList.Add(newItem);
ItemsRecreatedList = new List<ItemViewModel>(ItemsRecreatedList);
RaisePropertyChanged(nameof(ItemsRecreatedList));
}
}
}
还有ItemViewModel.cs
:
namespace UWP_ObservableCollection
{
public class ItemViewModel : BaseViewModel
{
private static int Counter;
public string Text { get; private set; }
public ItemViewModel()
{
Counter++;
Text = $"{Counter}";
}
}
}
这里是MainPage.xaml
:
<Page
x:Class="UWP_ObservableCollection.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP_ObservableCollection"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="Page_Loaded">
<StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items as List</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsAsList}"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items as ObservableCollection</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsAsObservableCollection}"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items recreated list</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsRecreatedList}"/>
</StackPanel>
<Button Click="Button_Click">Add new item</Button>
</StackPanel>
</Page>
MainPage.xaml.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace UWP_ObservableCollection
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainViewModel MainViewModel
{
get => DataContext as MainViewModel;
}
public MainPage()
{
this.InitializeComponent();
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
DataContext = new MainViewModel();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MainViewModel.AddNewItem();
}
}
}
MyItemsControl.xaml
:
<UserControl
x:Class="UWP_ObservableCollection.MyItemsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP_ObservableCollection"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<ItemsControl ItemsSource="{x:Bind ItemsSource, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
MyItemsControl.xaml.cs
:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
namespace UWP_ObservableCollection
{
public sealed partial class MyItemsControl : UserControl
{
// This works fine.
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IList<ItemViewModel>),
typeof(MyItemsControl),
new PropertyMetadata(null, ItemsSourcePropertyChanged)
);
public IList<ItemViewModel> ItemsSource
{
get { return (IList<ItemViewModel>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
// Uncomment this code to see the issue.
/*
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IList<BaseViewModel>),
typeof(MyItemsControl),
new PropertyMetadata(null, ItemsSourcePropertyChanged)
);
public IList<BaseViewModel> ItemsSource
{
get
{
var values = GetValue(ItemsSourceProperty) as IEnumerable<BaseViewModel>;
if (values is null)
{
return null;
}
return values.ToList();
}
set { SetValue(ItemsSourceProperty, value); }
}
*/
private static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("Items changed");
}
public MyItemsControl()
{
this.InitializeComponent();
}
}
}
您需要进行后续步骤:
- 构建并 运行 应用程序。
- 看到有 3 个
MyItemsControl
使用 3 个不同的数据源 - ItemsAsList
、ItemsAsObservableCollection
和 ItemsRecreatedList
。
检查 MainViewModel
发现,有 3 个来源:
IList<ItemViewModel> ItemsAsList
ObservableCollection<ItemViewModel> ItemsAsObservableCollection
IList<ItemViewModel> ItemsRecreatedList
- 点击 "Add new item"。您应该看到,第二个和第三个集合已更新。签入名为 `AddNewItem 的
MainViewModel
方法。
它应该将项目添加到每个集合中。
第一个问题:为什么item被添加到第一个collection,但是调用了RaisePropertyChanged却UI没有更新?
- 停止应用程序。
- 转到
MyItemsControl.xaml.cs
找到注释代码,取消注释并注释以前的代码。这会将 IList<ItemViewModel>
更改为 IList<BaseViewModel>
。
- 现在重建应用程序并再次运行。尝试再次单击 "Add new item" 并注意,
ObservableCollection
没有更新。 第二个问题:为什么ObservableCollection不再触发getter?
同样,您可以在 repo!
中找到所有这些
感谢您的帮助,也许我遗漏了什么!我对第二个问题很感兴趣,但不知道为什么它不起作用。希望你能帮助我!
Ань,为了让您的问题易于重现,这是一项艰巨的工作。干得好!
首先要记住的是集合绑定依赖于 2 个接口:INotifyPropertyChanged
and INotifyCollectionChanged
并且 ObservableCollection<T>
实现了这两个接口,而 IList<T>
两者都不实现。
INotifyCollectionChanged
的职责是通知事件订阅者有关实现它的集合中的添加、替换、移动或删除项目。
- Click on "Add new item". You should see, that 2nd and 3rd collections are updated. Check in MainViewModel method called `AddNewItem. It should add the item to each collection. First question: why the item is added to the first collection, but UI is not updated even if RaisePropertyChanged is called?
您将 1 个项目添加到 3 个集合支持的数据源。这里发生了什么:
- 1,
IList
数据源未触发 CollectionChanged
事件:绑定未收到任何更改通知,未发生 UI 更新。调用 RaisePropertyChanged(nameof(ItemsAsList));
没有任何作用,因为数据源对象 (ItemsAsList
) 保持不变,只是列表内容发生了变化。如果 IList
会实现 INotifyCollectionChanged
(它不会)这会起作用。
- 2,
ObservableCollection
数据源自动按预期工作:当新项目添加到集合中时,绑定会收到通知,并且项目会添加到 UI 列表中。
- 第三个数据源实际上重新创建了数据源集合,您通过
RaisePropertyChanged(nameof(ItemsRecreatedList));
手动通知绑定应该使用新的数据源集合。 UI 已更新,但与第二种情况相比,它不仅将 1 个项目添加到 UI 列表中,而是将整个列表重新填充到 UI 树中。
- Now rebuild app and run it once more. Try to click again on "Add new item" and notice, that ObservableCollection is not updated. Second question: why ObservableCollection doesn't trigger getter anymore?
在这里,您为依赖项 属性 使用自定义的 getter,它有时会调用集合上的 ToList()
方法,然后调用 returns 方法。 ToList
创建底层 ObservableCollection
内容的副本,现在与 MainViewModel
class 中的数据源分离并且属于 IList
类型,因此它a) 不知道视图模型集合中的后续更改,b) 无法通知 UI。
public IList<BaseViewModel> ItemsSource
{
get
{
var values = GetValue(ItemsSourceProperty) as IEnumerable<BaseViewModel>;
if (values is null)
{
return null;
}
return values.ToList();
}
set { SetValue(ItemsSourceProperty, value); }
}
我遇到了一些与 DependencyProperty
一起使用的 ObservableCollection
的奇怪行为。
我在这里创建了最小的可重现场景:https://github.com/aosyatnik/UWP_ObservableCollection_Issue.
有 2 个问题,我看到但无法解释。
这是我的 MainViewModel
:
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace UWP_ObservableCollection
{
public class MainViewModel : BaseViewModel
{
public IList<ItemViewModel> ItemsAsList { get; private set; }
public ObservableCollection<ItemViewModel> ItemsAsObservableCollection { get; private set; }
public IList<ItemViewModel> ItemsRecreatedList { get; private set; }
public MainViewModel()
{
ItemsAsList = new List<ItemViewModel>();
ItemsAsObservableCollection = new ObservableCollection<ItemViewModel>();
ItemsRecreatedList = new List<ItemViewModel>();
}
public void AddNewItem()
{
var newItem = new ItemViewModel();
// First try: add to list and raise property change - doesn't work.
ItemsAsList.Add(newItem);
RaisePropertyChanged(nameof(ItemsAsList));
// Second try: with ObservableCollection - doesn't work?
ItemsAsObservableCollection.Add(newItem);
// Third try: recreate the whole collection - works
ItemsRecreatedList.Add(newItem);
ItemsRecreatedList = new List<ItemViewModel>(ItemsRecreatedList);
RaisePropertyChanged(nameof(ItemsRecreatedList));
}
}
}
还有ItemViewModel.cs
:
namespace UWP_ObservableCollection
{
public class ItemViewModel : BaseViewModel
{
private static int Counter;
public string Text { get; private set; }
public ItemViewModel()
{
Counter++;
Text = $"{Counter}";
}
}
}
这里是MainPage.xaml
:
<Page
x:Class="UWP_ObservableCollection.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP_ObservableCollection"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="Page_Loaded">
<StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items as List</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsAsList}"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items as ObservableCollection</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsAsObservableCollection}"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock>Items recreated list</TextBlock>
<local:MyItemsControl ItemsSource="{Binding ItemsRecreatedList}"/>
</StackPanel>
<Button Click="Button_Click">Add new item</Button>
</StackPanel>
</Page>
MainPage.xaml.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace UWP_ObservableCollection
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainViewModel MainViewModel
{
get => DataContext as MainViewModel;
}
public MainPage()
{
this.InitializeComponent();
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
DataContext = new MainViewModel();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MainViewModel.AddNewItem();
}
}
}
MyItemsControl.xaml
:
<UserControl
x:Class="UWP_ObservableCollection.MyItemsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP_ObservableCollection"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<ItemsControl ItemsSource="{x:Bind ItemsSource, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
MyItemsControl.xaml.cs
:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
namespace UWP_ObservableCollection
{
public sealed partial class MyItemsControl : UserControl
{
// This works fine.
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IList<ItemViewModel>),
typeof(MyItemsControl),
new PropertyMetadata(null, ItemsSourcePropertyChanged)
);
public IList<ItemViewModel> ItemsSource
{
get { return (IList<ItemViewModel>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
// Uncomment this code to see the issue.
/*
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IList<BaseViewModel>),
typeof(MyItemsControl),
new PropertyMetadata(null, ItemsSourcePropertyChanged)
);
public IList<BaseViewModel> ItemsSource
{
get
{
var values = GetValue(ItemsSourceProperty) as IEnumerable<BaseViewModel>;
if (values is null)
{
return null;
}
return values.ToList();
}
set { SetValue(ItemsSourceProperty, value); }
}
*/
private static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("Items changed");
}
public MyItemsControl()
{
this.InitializeComponent();
}
}
}
您需要进行后续步骤:
- 构建并 运行 应用程序。
- 看到有 3 个
MyItemsControl
使用 3 个不同的数据源 -ItemsAsList
、ItemsAsObservableCollection
和ItemsRecreatedList
。 检查MainViewModel
发现,有 3 个来源:IList<ItemViewModel> ItemsAsList
ObservableCollection<ItemViewModel> ItemsAsObservableCollection
IList<ItemViewModel> ItemsRecreatedList
- 点击 "Add new item"。您应该看到,第二个和第三个集合已更新。签入名为 `AddNewItem 的
MainViewModel
方法。 它应该将项目添加到每个集合中。 第一个问题:为什么item被添加到第一个collection,但是调用了RaisePropertyChanged却UI没有更新? - 停止应用程序。
- 转到
MyItemsControl.xaml.cs
找到注释代码,取消注释并注释以前的代码。这会将IList<ItemViewModel>
更改为IList<BaseViewModel>
。 - 现在重建应用程序并再次运行。尝试再次单击 "Add new item" 并注意,
ObservableCollection
没有更新。 第二个问题:为什么ObservableCollection不再触发getter?
同样,您可以在 repo!
中找到所有这些感谢您的帮助,也许我遗漏了什么!我对第二个问题很感兴趣,但不知道为什么它不起作用。希望你能帮助我!
Ань,为了让您的问题易于重现,这是一项艰巨的工作。干得好!
首先要记住的是集合绑定依赖于 2 个接口:INotifyPropertyChanged
and INotifyCollectionChanged
并且 ObservableCollection<T>
实现了这两个接口,而 IList<T>
两者都不实现。
INotifyCollectionChanged
的职责是通知事件订阅者有关实现它的集合中的添加、替换、移动或删除项目。
- Click on "Add new item". You should see, that 2nd and 3rd collections are updated. Check in MainViewModel method called `AddNewItem. It should add the item to each collection. First question: why the item is added to the first collection, but UI is not updated even if RaisePropertyChanged is called?
您将 1 个项目添加到 3 个集合支持的数据源。这里发生了什么:
- 1,
IList
数据源未触发CollectionChanged
事件:绑定未收到任何更改通知,未发生 UI 更新。调用RaisePropertyChanged(nameof(ItemsAsList));
没有任何作用,因为数据源对象 (ItemsAsList
) 保持不变,只是列表内容发生了变化。如果IList
会实现INotifyCollectionChanged
(它不会)这会起作用。 - 2,
ObservableCollection
数据源自动按预期工作:当新项目添加到集合中时,绑定会收到通知,并且项目会添加到 UI 列表中。 - 第三个数据源实际上重新创建了数据源集合,您通过
RaisePropertyChanged(nameof(ItemsRecreatedList));
手动通知绑定应该使用新的数据源集合。 UI 已更新,但与第二种情况相比,它不仅将 1 个项目添加到 UI 列表中,而是将整个列表重新填充到 UI 树中。
- Now rebuild app and run it once more. Try to click again on "Add new item" and notice, that ObservableCollection is not updated. Second question: why ObservableCollection doesn't trigger getter anymore?
在这里,您为依赖项 属性 使用自定义的 getter,它有时会调用集合上的 ToList()
方法,然后调用 returns 方法。 ToList
创建底层 ObservableCollection
内容的副本,现在与 MainViewModel
class 中的数据源分离并且属于 IList
类型,因此它a) 不知道视图模型集合中的后续更改,b) 无法通知 UI。
public IList<BaseViewModel> ItemsSource
{
get
{
var values = GetValue(ItemsSourceProperty) as IEnumerable<BaseViewModel>;
if (values is null)
{
return null;
}
return values.ToList();
}
set { SetValue(ItemsSourceProperty, value); }
}