Home > Enterprise >  IEnumerable changed event
IEnumerable changed event

Time:01-26

I am programming an app in WinUI 3 using C#.

There are some controls that have an ItemsSource property.
When I pass an IEnumerable into the ItemsSource and then change the IEnumerable, the control changes accordingly.

I now want to implement the same behavior in my custom UserControl.
I have the following code, but it only works with IEnumerables that implement INotifyCollectionChanged (eg. ObservableCollection).
WinUI supports IEnumerables that don't implement INotifyCollectionChanged.
How can I do the same?

Here is my code:

public sealed partial class MyUserControl : UserControl
{
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MyUserControl), new PropertyMetadata(null));
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    private IEnumerable _oldItemsSource;

    public MyUserControl()
    {
        this.InitializeComponent();
        RegisterPropertyChangedCallback(ItemsSourceProperty, OnItemsSourceChanged);
    }

    private void OnItemsSourceChanged(DependencyObject sender, DependencyProperty prop)
    {
        if (prop == ItemsSourceProperty)
        {
            var newValue = (IEnumerable)sender.GetValue(ItemsSourceProperty);

            if (_oldItemsSource is INotifyCollectionChanged oldCollection)
            {
                oldCollection.CollectionChanged -= OnCollectionChanged;
            }
            if (newValue is INotifyCollectionChanged collection)
            {
                collection.CollectionChanged  = OnCollectionChanged;
            }
            _oldItemsSource = ItemsSource;
        }
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Update the control here
    }
}

WinUI 3 allows me to use a List (doesn't implement INotifyCollectionChanged) as an ItemsSource.
Changes made to the List affect the control.
This is the code inside a test page:

public TestPage()
{
    this.InitializeComponent();

    var list = new List<string> { "Item1", "Item2", "Item3" };

    var bar = new BreadcrumbBar(); ;
    bar.ItemsSource = list;

    this.Content = bar;

    list.Add("Item4");
    // The BreadcrumbBar now has 4 elements.
}

CodePudding user response:

When I pass an IEnumerable into the ItemsSource and then change the IEnumerable, the control changes accordingly.

No, it doesn't unless the IEnumerable is an INotifyCollectionChanged.

Try to call list.Add("Item4") in an event handler after the control has been initially rendered if you don't believe me.

For example, this code will not add "Item4" to the BreadcrumbBar control:

public sealed partial class TestPage : Page
{
    private readonly List<string> list = new List<string> { "Item1", "Item2", "Item3" };

    public TestPage()
    {
        this.InitializeComponent();
        breadcrumbBar.ItemsSource = list;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        list.Add("Item4");
    }
}

Changing the type of list to ObservableCollection<string> will make it work as expected.

So your custom control is no worse than any built-in control in that sense. The source collection must notify the view somehow.

CodePudding user response:

WinUI 3 allows me to use a List (doesn't implement INotifyCollectionChanged) as an ItemsSource. Changes made to the List affect the control.

It most certainly doesn't react to changes of "dumb" collections. Your example only works because the items haven't been constructed yet, and they won't be until the control is loaded (your code is all in the constructor).

CodePudding user response:

This example will show you a comparison between a plain List and an ObservableCollection.

Let's say we have a UserControl like this one.

TestUserControl.xaml

<UserControl
    x:Class="ItemsSourceTest.TestUserControl"
    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:ItemsSourceTest"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <ListView ItemsSource="{x:Bind ItemsSource, Mode=OneWay}" />
    </Grid>

</UserControl>

TestUserControl.xaml.cs

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System.Collections;
using Windows.Foundation.Collections;

namespace ItemsSourceTest;

public sealed partial class TestUserControl : UserControl
{
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
        nameof(ItemsSource),
        typeof(IEnumerable),
        typeof(TestUserControl),
        new PropertyMetadata(default, (d, e) => (d as TestUserControl)?.UpdateItemsSource()));

    public TestUserControl()
    {
        this.InitializeComponent();
    }

    // You don't need CollectionViewSource or CollectionView
    // in order to populate the ListView. This is just to show 
    // you how you can get events when the collection changes.
    public CollectionViewSource? CollectionViewSource { get; set; }

    public ICollectionView? CollectionView { get; set; }

    public IEnumerable ItemsSource
    {
        get => (IEnumerable)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    private void UpdateItemsSource()
    {
        CollectionViewSource = new CollectionViewSource()
        {
            Source = ItemsSource,
        };

        if (CollectionView is not null)
        {
            CollectionView.VectorChanged -= CollectionView_VectorChanged;
        }

        CollectionView = CollectionViewSource.View;
        CollectionView.VectorChanged  = CollectionView_VectorChanged;
    }

    private void CollectionView_VectorChanged(IObservableVector<object> sender, IVectorChangedEventArgs @event)
    {
        // Your can do your work for collection changes here...
    }
}

And a MainPage a ListView and a TestUserControl both bound to a plain List named NonObservableItems and another set of a ListView and a TestUserControl both bound to an ObservableCollection named ObservableItems.

When you add items to the collections, you'll see that only the controls bound to the ObservableItems will be populated.

MainPage.xaml

<Page
    x:Class="ItemsSourceTest.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:ItemsSourceTest"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid RowDefinitions="Auto,*">
        <Button
            Grid.Row="0"
            Click="Button_Click"
            Content="Add item" />
        <Grid
            Grid.Row="1"
            ColumnDefinitions="*,*"
            RowDefinitions="Auto,*,*">
            <TextBlock
                Grid.Row="0"
                Grid.Column="0"
                Text="Non-Observable Items" />
            <ListView
                Grid.Row="1"
                Grid.Column="0"
                ItemsSource="{x:Bind NonObservableItems, Mode=OneWay}" />
            <local:TestUserControl
                Grid.Row="2"
                Grid.Column="0"
                ItemsSource="{x:Bind NonObservableItems, Mode=OneWay}" />

            <TextBlock
                Grid.Row="0"
                Grid.Column="1"
                Text="Observable Items" />
            <ListView
                Grid.Row="1"
                Grid.Column="1"
                ItemsSource="{x:Bind ObservableItems, Mode=OneWay}" />
            <local:TestUserControl
                Grid.Row="2"
                Grid.Column="1"
                ItemsSource="{x:Bind ObservableItems, Mode=OneWay}" />
        </Grid>

    </Grid>
</Page>

MainPage.xaml.cs

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ItemsSourceTest;

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

    public List<string> NonObservableItems { get; set; } = new();

    public ObservableCollection<string> ObservableItems { get; set; } = new();

    private int Counter { get; set; } = 0;

    private void AddItem()
    {
        NonObservableItems.Add(Counter.ToString());
        ObservableItems.Add(Counter.ToString());
        Counter  ;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        AddItem();
    }
}
  • Related