I'm facing some weird behavior of ObservableCollection
, that is used with DependencyProperty
.
I've created minimal reproducible scenario here: https://github.com/aosyatnik/UWP_ObservableCollection_Issue.
There are 2 issues, that I see and can not explain.
Here is my 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));
}
}
}
Also ItemViewModel.cs
:
namespace UWP_ObservableCollection
{
public class ItemViewModel : BaseViewModel
{
private static int Counter;
public string Text { get; private set; }
public ItemViewModel()
{
Counter++;
Text = $"{Counter}";
}
}
}
Here is 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();
}
}
}
You need to make the next steps:
MyItemsControl
that are using 3 different data sources - ItemsAsList
, ItemsAsObservableCollection
and ItemsRecreatedList
.
Check MainViewModel
and find, that there are 3 sources:
IList<ItemViewModel> ItemsAsList
ObservableCollection<ItemViewModel> ItemsAsObservableCollection
IList<ItemViewModel> ItemsRecreatedList
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?MyItemsControl.xaml.cs
find commented code, uncomment it and comment previous code. This changes IList<ItemViewModel>
to IList<BaseViewModel>
.ObservableCollection
is not updated. Second question: why ObservableCollection doesn't trigger getter anymore?I appreciate your help, maybe I'm missing something! I'm very interested in the second question and have no idea why it doesn't work. Hope, that you will help me!
Ань, that's quite a work to make your issue easy to reproduce. Good job!
First thing to keep in mind is that collection binding relies on 2 interfaces: INotifyPropertyChanged
and INotifyCollectionChanged
and that ObservableCollection<T>
implements both of them, while IList<T>
implements neither.
Responsibilities of the INotifyCollectionChanged
is to notify event subscribers about added, replaced, moved, or deleted items in a collection that implements it.
- 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?
You add 1 item to 3 collection-backed data sources. Here what happens:
IList
data source doesn't fire CollectionChanged
event: binding is not notified of any changes, no UI updates happen. A call to RaisePropertyChanged(nameof(ItemsAsList));
does nothing, since data source object (ItemsAsList
) remains the same, it's only list content that changes. If IList
would implement INotifyCollectionChanged
(it doesn't) this would work.ObservableCollection
data source automatically works as expected: when a new item is added to the collection, binding is notified and an item is added to the UI list.RaisePropertyChanged(nameof(ItemsRecreatedList));
that the new data source collection should be used. UI is updated, but in comparison to the 2nd case it's not just 1 item added to the UI list, but an entire list is re-populated in UI tree.
- 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?
Here you use a customized getter for the dependency property, which at some point calls ToList()
method on a collection and returns that. ToList
creates a copy of the underlying ObservableCollection
content, which is now detached from the data source in the MainViewModel
class and is of IList
type, so it a) is unaware of subsequent changes in the view-model collection and b) can not notify UI about it.
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); }
}