Я программирую приложение в WinUI 3, используя С#.
Некоторые элементы управления имеют свойство ItemsSource.
Когда я передаю IEnumerable в ItemsSource, а затем изменяю IEnumerable, элемент управления изменяется соответствующим образом.
Теперь я хочу реализовать такое же поведение в своем пользовательском элементе управления UserControl.
У меня есть следующий код, но он работает только с IEnumerables, которые реализуют INotifyCollectionChanged (например, ObservableCollection).
WinUI поддерживает IEnumerables, которые не реализуют INotifyCollectionChanged.
Как я могу сделать то же самое?
Вот мой код:
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 позволяет мне использовать список (не реализует INotifyCollectionChanged) в качестве источника элементов.
Изменения, внесенные в Список, влияют на управление.
Это код внутри тестовой страницы:
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.
}





WinUI 3 позволяет мне использовать список (не реализует INotifyCollectionChanged) в качестве ItemsSource. Изменения, внесенные в список, влияют на элемент управления.
На изменения "тупых" коллекций он уж точно не реагирует. Ваш пример работает только потому, что элементы еще не были созданы, и они не будут созданы до тех пор, пока элемент управления не будет загружен (весь ваш код находится в конструкторе).
Этот пример покажет вам сравнение между простым List и ObservableCollection.
Допустим, у нас есть такой UserControl.
Тестусерконтрол.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>
Тестусерконтрол.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...
}
}
И MainPage, и ListView, и TestUserControl связаны с простым List с именем NonObservableItems, а другой набор ListView и TestUserControl связан с ObservableCollection с именем ObservableItems.
Когда вы добавляете элементы в коллекции, вы увидите, что будут заполнены только элементы управления, привязанные к ObservableItems.
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();
}
}
Когда я передаю IEnumerable в ItemsSource, а затем изменяю IEnumerable, элемент управления изменяется соответствующим образом.
Нет, если IEnumerable не является INotifyCollectionChanged.
Попробуйте вызвать list.Add("Item4") в обработчике событий после первоначального рендеринга элемента управления, если вы мне не верите.
Например, этот код не добавит «Item4» в элемент управления BreadcrumbBar:
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");
}
}
Изменение типа list на ObservableCollection<string> заставит его работать должным образом.
Так что в этом смысле ваш пользовательский элемент управления не хуже любого встроенного элемента управления. Исходная коллекция должна каким-то образом уведомлять представление.
Я предполагаю, что этот код работает, потому что список визуально загружается в панель после завершения функции, и основной поток снова может работать.