使用基本抽象 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();
        }
    }
}

您需要进行后续步骤:

  1. 构建并 运行 应用程序。
  2. 看到有 3 个 MyItemsControl 使用 3 个不同的数据源 - ItemsAsListItemsAsObservableCollectionItemsRecreatedList。 检查 MainViewModel 发现,有 3 个来源:
    • IList<ItemViewModel> ItemsAsList
    • ObservableCollection<ItemViewModel> ItemsAsObservableCollection
    • IList<ItemViewModel> ItemsRecreatedList
  3. 点击 "Add new item"。您应该看到,第二个和第三个集合已更新。签入名为 `AddNewItem 的 MainViewModel 方法。 它应该将项目添加到每个集合中。 第一个问题:为什么item被添加到第一个collection,但是调用了RaisePropertyChanged却UI没有更新?
  4. 停止应用程序。
  5. 转到 MyItemsControl.xaml.cs 找到注释代码,取消注释并注释以前的​​代码。这会将 IList<ItemViewModel> 更改为 IList<BaseViewModel>
  6. 现在重建应用程序并再次运行。尝试再次单击 "Add new item" 并注意,ObservableCollection 没有更新。 第二个问题:为什么ObservableCollection不再触发getter?

同样,您可以在 repo!

中找到所有这些

感谢您的帮助,也许我遗漏了什么!我对第二个问题很感兴趣,但不知道为什么它不起作用。希望你能帮助我!

Ань,为了让您的问题易于重现,这是一项艰巨的工作。干得好!

首先要记住的是集合绑定依赖于 2 个接口:INotifyPropertyChanged and INotifyCollectionChanged 并且 ObservableCollection<T> 实现了这两个接口,而 IList<T> 两者都不实现。
INotifyCollectionChanged 的职责是通知事件订阅者有关实现它的集合中的添加、替换、移动或删除项目。

  1. 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 树中。
  1. 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); }
}