username username - 3 days ago 5
C# Question

Cannot bind to a collection from ItemsControl template

I'm trying to bind a user control property "MyUserControl.Names" to a collection property "Names" of the main window. It doesn't work if I do it in ItemsControl template, but it works if I move the control definition out of the ItemsControl template. Here is the xaml:

<Window x:Class="TestItemsControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestItemsControl"
Height="200" Width="200"
Name="MainControl">
<StackPanel>
<ItemsControl ItemsSource="{Binding Groups, ElementName=MainControl}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- This doesn't work -->
<StackPanel Background="WhiteSmoke" Height="40" Width="100" Margin="5" HorizontalAlignment="Left">
<TextBlock Text="{Binding .}"/>
<local:MyUserControl Names="{Binding Names, ElementName=MainControl}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- This works -->
<StackPanel Background="WhiteSmoke" Height="40" Width="100" Margin="5" HorizontalAlignment="Left">
<TextBlock Text="Group3"/>
<local:MyUserControl Names="{Binding Names, ElementName=MainControl}"/>
</StackPanel>
</StackPanel>
</Window>


MainWindow.xaml.cs contains two dependency properties:

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SetValue(GroupsProperty, new ObservableCollection<string>());
SetValue(NamesProperty, new ObservableCollection<string>());

Groups.Add("Group1");
Groups.Add("Group2");
Names.Add("Name1");
Names.Add("Name2");
}

public static readonly DependencyProperty GroupsProperty =
DependencyProperty.Register("Groups", typeof(ObservableCollection<string>), typeof(MainWindow),
new PropertyMetadata(null));

public ObservableCollection<string> Groups
{
get { return (ObservableCollection<string>)GetValue(GroupsProperty); }
set { SetValue(GroupsProperty, value); }
}

public static readonly DependencyProperty NamesProperty =
DependencyProperty.Register("Names", typeof(ObservableCollection<string>), typeof(MainWindow),
new PropertyMetadata(null));

public ObservableCollection<string> Names
{
get { return (ObservableCollection<string>)GetValue(NamesProperty); }
set { SetValue(NamesProperty, value); }
}
}


Here is the result:

enter image description here

The first two rectangles are what ItemsControl generates. The third one is what I have manually added right after the ItemsControl. As you can see, even though the code is exactly the same in both cases, the first two rectangles don't have names, but the third one has. Is there any reason why wouldn't it work with ItemsControl?

Edit:

Here is the code of the MyUserControl.xaml.cs:

public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
SetValue(NamesProperty, new ObservableCollection<string>());
}

public static readonly DependencyProperty NamesProperty = DependencyProperty.Register(
"Names", typeof(ObservableCollection<string>), typeof(MyUserControl),
new PropertyMetadata(null, NamesPropertyChanged));

public ObservableCollection<string> Names
{
get { return (ObservableCollection<string>)GetValue(NamesProperty); }
set { SetValue(NamesProperty, value); }
}

private static void NamesPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var control = (MyUserControl)obj;
var oldCollection = e.OldValue as INotifyCollectionChanged;
var newCollection = e.NewValue as INotifyCollectionChanged;

if (oldCollection != null)
oldCollection.CollectionChanged -= control.NamesCollectionChanged;
if (newCollection != null)
newCollection.CollectionChanged += control.NamesCollectionChanged;

control.UpdateNames();
}

private void NamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateNames();
}

private void UpdateNames()
{
NamesPanel.Children.Clear();

if (Names == null)
return;

foreach(var name in Names)
{
var textBlock = new TextBlock();
textBlock.Text = name + ", ";
NamesPanel.Children.Add(textBlock);
}
}
}


MyUserControl.xaml:

<UserControl x:Class="TestItemsControl.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestItemsControl"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300"
Name="ParentControl">
<StackPanel Name="NamesPanel" Orientation="Horizontal"/>
</UserControl>

Answer

Replace SetValue in the UserControl's constructor by SetCurrentValue. It may even make sense not to assign an initial value at all for the Names property.

public MyUserControl()
{
    InitializeComponent();
    SetCurrentValue(NamesProperty, new ObservableCollection<string>());
}

SetValue (as opposed to SetCurrentValue) sets a so-called local value to the Names property. When you assign a Binding as in the second case, this is also considered a local value with the same precedence as the one set in the constructor.

However, in the first case, the Binding is set in a DataTemplate, where it doesn't count as a local value. Since it has lower precedence, it does not replace the initial value.

More details here: Dependency Property Value Precedence

Comments