Search code examples
wpfdata-bindingborderitemscontrol

RelativeSource binding in ItemsControl doesn't work


I'm stuck on the following binding problem. I have the following class declarations in MainWindow.xaml.cs:

    public List<MyArrayClass> MyArrayData { get; set; }
    public class MyArrayClass
    {
        public ObservableCollection<int> MyArrayInt { get; set; }
        public ObservableCollection<Thickness> MyArrayBorder { get; set; }
    }

And I create the object as follows:

        MyArrayClass sample3 = new MyArrayClass
        {
            MyArrayInt = new ObservableCollection<int> { 0, 1, 2, 3 },
            MyArrayBorder = new ObservableCollection<Thickness> { new Thickness(0,0,0,2), new Thickness(0, 0, 0, 2), new Thickness(0, 0, 0, 2), new Thickness(0, 0, 0, 2) }
        };

        MyArrayData = new List<MyArrayClass>();
        MyArrayData.Add(sample3);

Having said that, my objective is to display the contents of MyArrayInt into a set of TextBlocks using the ItemsControl object (I'm trying to understand how it works). In particular, I want to be able to define the border of each TextBlock via binding. My XAML looks like:

<ItemsControl Name="List" ItemsSource="{Binding MyArrayData}" Grid.Row="1" Grid.Column="1">
        <!-- List<> -->
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ItemsControl Name="MyArrayInt" ItemsSource="{Binding MyArrayInt}">
                    <!-- Array of int-->
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Border BorderBrush="Red" BorderThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}, AncestorLevel=2}, Path=MyArrayBorder}">
                                <TextBlock Text="{Binding Path=.}" Background="Azure" Margin="2,0,0,0"/>
                            </Border>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

The result is that MyArrayInt is displayed as I expect, but I get the following error on the BorderThickness binding:

System.Windows.Data Error: 40 : BindingExpression path error: 'MyArrayBorder' property not found on 'object' ''ItemsControl' (Name='List')'. BindingExpression:Path=MyArrayBorder; DataItem='ItemsControl' (Name='List'); target element is 'Border' (Name=''); target property is 'BorderThickness' (type 'Thickness')

The way I read it, it seems that the RelativeSource binding is correct: the Name of the ItemsControl object is 'List', and it's the one binding to MyArrayData. So why doesn't it find MyArrayBorder?

Any suggestion or advice is welcome!


Solution

  • Instead of having two collection properties in MyArrayClass, you should create a custom type and merge them into one:

    public class MyArrayClass
    {
        public ObservableCollection<MyType> MyArray { get; set; }
    }
    ...
    public class MyType
    {
        public int MyArrayInt { get; set; }
        public Thickness MyArrayBorder { get; set; }
    }
    

    You could then easily bind to each of the properties in the new type:

    <ItemsControl Name="MyArrayInt" ItemsSource="{Binding MyArray}">
        <!-- Array of int-->
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Red" BorderThickness="{Binding MyArrayBorder}">
                    <TextBlock Text="{Binding MyArrayInt}" Background="Azure" Margin="2,0,0,0"/>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
    

    Given your current setup, you need to use a converter to be able to get a specific item from the MyArrayBorder collection of the MyArrayClass.

    You can indeed easily bind to the MyArrayBorder property itself:

    BorderThickness="{Binding Path=MyArrayBorder[0], 
        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}"
    

    ...but you cannot bind to an item at a dynamic index (to replace 0 with in the above snipper) in it using pure XAML.