在 WPF 用的多的列表控件如 ListBox 或 ListView 等,本文告訴大家在這些列表控件上進行綁定多個數據集合來源的多個實現方法。如有一個顯示動物列表的控件,需要綁定的數據來源是阿貓和阿狗兩個 ObservableCollection 列表,不在后台代碼編寫合並集合的代碼情況下,可以通過 XAML 的編寫,綁定多個數據集合
准備
在開始之前,咱先搭建一點測試使用的代碼,假定咱有一個 列表控件 准備綁定到的數據源是兩個 ObservableCollection 對象,下面來定義這兩個 ObservableCollection 對象和對應的 阿貓和阿狗 的代碼
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
{
Dogs.Add(new Dog()
{
Name = "Dog" + i
});
Cats.Add(new Cat()
{
Name = "Cat" + i
});
}
DataContext = this;
}
public ObservableCollection<Dog> Dogs { get; } = new ObservableCollection<Dog>();
public ObservableCollection<Cat> Cats { get; } = new ObservableCollection<Cat>();
}
public class Dog : Animal
{
}
public class Cat : Animal
{
}
public class Animal
{
public string Name { get; set; }
}
可以看到以上代碼里面存在兩個 ObservableCollection 對象,同時 MainWindow 的 DataContext 就是 MainWindow 對象。咱需要將兩個 ObservableCollection 對象作為數據源,放在相同的一個 ListBox 里面
下面是多個不同的實現方式,解決如何在 WPF 中在 ListBox 或 ListView 綁定多個數據集合 ObservableCollection 對象
通過 CollectionViewSource 方式
在 ListView 或 ListBox 資源里面,添加 CollectionViewSource 綁定到集合里面,然后在 ItemsSource 使用 CompositeCollection 進行綁定,代碼如下
<ListBox>
<ListBox.Resources>
<CollectionViewSource x:Key="DogCollection" Source="{Binding Dogs}"/>
<CollectionViewSource x:Key="CatCollection" Source="{Binding Cats}"/>
</ListBox.Resources>
<ListBox.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource DogCollection}}"/>
<CollectionContainer Collection="{Binding Source={StaticResource CatCollection}}"/>
</CompositeCollection>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
這個方法的優勢在於可以完全使用 XAML 編寫內容,但是缺點在於有重復的代碼,如有多個綁定的集合對象,就需要在資源和 CompositeCollection 里面定義多個 CollectionViewSource 和 CollectionContainer 對象
如果綁定的集合數量不多,那么此寫法還成,但如果集合數量比較多,而且需要不斷變更順序,那以上寫法就有坑
此方法請參考 WPF 很少人知道的科技 - walterlv
通過 CompositeCollection 動態綁定
在 ListView 或 ListBox 的資源里面定義了 CompositeCollection 通過控件的 DataContext 綁定多個集合,代碼如下
<CompositeCollection x:Key="MyColl">
<CollectionContainer Collection="{Binding DataContext.Dogs, Source={x:Reference MyList}}"/>
<CollectionContainer Collection="{Binding DataContext.Cats, Source={x:Reference MyList}}"/>
</CompositeCollection>
以上代碼的 MyList 就是集合控件,此方法需要用到 x:Reference
獲取對象的引用,同時需要通過 DataContext
的某個屬性獲取到對應的屬性,全部代碼如下
<ListBox x:Name="MyList" ItemsSource="{DynamicResource MyColl}">
<ListBox.Resources>
<CompositeCollection x:Key="MyColl">
<CollectionContainer Collection="{Binding DataContext.Dogs, Source={x:Reference MyList}}"/>
<CollectionContainer Collection="{Binding DataContext.Cats, Source={x:Reference MyList}}"/>
</CompositeCollection>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
對比上面的方法,此方法可以讓綁定集合的代碼只寫一次,看起來代碼更少一點。但不足的地方在於綁定 ItemsSource 需要用到 DynamicResource 的方式,相對性能不如上面方法。為什么需要 DynamicResource 資源?原因是資源本身定義在 Resources 里面。為什么資源需要定義在控件里面的 Resource 里面?原因是為了獲取到控件的 x:Reference
對象。也就是說需要在控件創建出來之后,才能通過 x:Reference
獲取控件,而控件的數據內容需要依賴資源的定義,因此也只有以上方式的寫法
如果能從控件的上層容器拿到數據對象,那可以將資源定義在容器里面,通過 StaticResource 綁定到靜態資源。如放在 Window 的 Resources 里
<Window x:Class="CibairyafocairluYerkinemde.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CibairyafocairluYerkinemde"
mc:Ignorable="d"
x:Name="Root"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<CompositeCollection x:Key="MyColl">
<CollectionContainer Collection="{Binding DataContext.Dogs, Source={x:Reference Root}}"/>
<CollectionContainer Collection="{Binding DataContext.Cats, Source={x:Reference Root}}"/>
</CompositeCollection>
</Window.Resources>
<Grid>
<ListBox x:Name="MyList" ItemsSource="{StaticResource MyColl}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
以上寫法沒有啥缺點,也不存在動態資源的性能問題。但實際上在有動態資源下,性能問題也是很小的問題,對比渲染控件本身,動態綁定性能可以忽略
通過多綁定方法
此方法需要添加一點后台代碼,定義 CompositeCollectionConverter 轉換器,實現邏輯是通過多綁定的方法,將多個數據集合當成多個參數進行綁定
<ListBox>
<ListBox.ItemsSource>
<MultiBinding Converter="{x:Static local:CompositeCollectionConverter.Default}">
<Binding Path="Dogs" />
<Binding Path="Cats" />
</MultiBinding>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
可以看到此方法的 XAML 代碼量最小,只是需要一個輔助的 CompositeCollectionConverter 類,代碼如下
public class CompositeCollectionConverter : IMultiValueConverter
{
public static readonly CompositeCollectionConverter Default = new CompositeCollectionConverter();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var compositeCollection = new CompositeCollection();
foreach (var value in values)
{
if (value is IEnumerable enumerable)
{
compositeCollection.Add(new CollectionContainer { Collection = enumerable });
}
else
{
compositeCollection.Add(value);
}
}
return compositeCollection;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException("CompositeCollectionConverter ony supports oneway bindings");
}
}
可以將 CompositeCollectionConverter 放在庫里面,這樣就可以讓 XAML 代碼看起來簡單
本文所有代碼放在 github 和 gitee 歡迎小伙伴訪問
參考
本文以上方法參考了如下博客
wpf - How do you bind a CollectionContainer to a collection in a view model? - Stack Overflow