This really is striking me hard right now ...
Context
I am currently developing an application, where i need to combine multiple collections (Receipt.Contact.Addresses
, Receipt.Contact.MainAddress
via converter into collection) into a single source for a combobox (Receipt.BillingAddress
).
The Problem
The real application has Receipt.BillingAddress
bound to the SelectedItem
property of a ComboBox
with the described CompositeCollection
.
Changing the Receipt.Contact
then will erase the Receipt.BillingAddress
as Selector
simply works like that.
This however, introduces random behavior, aka issues, due to async IO (server receives null update, sends out null update, server receives another update, ...)
Theoretically, this could be fixed by detaching and reattaching the binding everytime, the actual collection changes (hence the ItemsSourceAttached)
Sadly, this is not working as the PropertyChangedHandler
is only ever fired the very first time it gets changed.
The weird stuff
This is fully working, if there is no extra levels inside the CollectionViewSource.Source
binding (Receipt.Contact.Addresses
vs Addresses
)
How To Reproduce (Minimum Viable Example)
To reproduce this behavior, i created the following MVE consisting out of 3 Classes (Window, AttachedProperty and SomeContainer) and a single, XAML file (Window):
The AttachedProperty
public static class ItemsSourceAttached
{
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached(
nameof(Selector.ItemsSource),
typeof(IEnumerable),
typeof(ItemsSourceAttached),
new FrameworkPropertyMetadata(null, ItemsSourcePropertyChanged)
);
public static void SetItemsSource(Selector element, IEnumerable value)
{
element.SetValue(ItemsSourceProperty, value);
}
public static IEnumerable GetItemsSource(Selector element)
{
return (IEnumerable)element.GetValue(ItemsSourceProperty);
}
static void ItemsSourcePropertyChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
MessageBox.Show("Attached Changed!");
if (element is Selector target)
{
target.ItemsSource = e.NewValue as IEnumerable;
}
}
}
SomeContainer
public class SomeContainer : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string[] Data1 { get; }
public string[] Data2 { get; }
public SomeContainer(string[] data1, string[] data2)
{
this.Data1 = data1;
this.Data2 = data2;
}
}
The Window (C#) & DataContext (for simplicity)
public partial class CompositeCollectionTest : Window, INotifyPropertyChanged
{
public SomeContainer Data
{
get => this._Data;
set
{
this._Data = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Data)));
}
}
private SomeContainer _Data;
// Not allowed to be NULLed on ItemsSource change
public string SelectedItem
{
get => this._SelectedItem;
set
{
this._SelectedItem = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.SelectedItem)));
}
}
private string _SelectedItem;
public bool SomeProperty => false;
public event PropertyChangedEventHandler PropertyChanged;
public CompositeCollectionTest()
{
this.InitializeComponent();
var descriptor = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(Selector));
descriptor.AddValueChanged(this.MyComboBox, (sender, e) => {
MessageBox.Show("Property Changed!");
});
}
static int i = 0;
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Data = new SomeContainer(new string[]
{
$"{i}-DATA-A-1",
$"{i}-DATA-A-2",
$"{i}-DATA-A-3"
},
new string[]
{
$"{i}-DATA-B-1",
$"{i}-DATA-B-2",
$"{i}-DATA-B-3"
});
i++;
}
}
The Window (XAML):
<Window x:Class="WpfTest.CompositeCollectionTest"
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:WpfTest"
mc:Ignorable="d"
Title="CompositeCollectionTest"
Height="450" Width="800"
DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">
<Window.Resources>
<CollectionViewSource x:Key="ViewSource1" Source="{Binding Data.Data1}"/>
<CollectionViewSource x:Key="ViewSource2" Source="{Binding Data.Data2}"/>
</Window.Resources>
<StackPanel>
<ComboBox x:Name="MyComboBox" SelectedItem="{Binding SelectedItem}">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SomeProperty}" Value="False">
<Setter Property="local:ItemsSourceAttached.ItemsSource">
<Setter.Value>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource ViewSource1}}"/>
<CollectionContainer Collection="{Binding Source={StaticResource ViewSource2}}"/>
</CompositeCollection>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<Button Content="Generate" Click="Button_Click"/>
</StackPanel>
</Window>
Thank you for your time already. And i really hope someone can point me at my obvious mistake that i cannot seem to find...