2

I am trying to create a usercontrol in WPF that includes a content collection and struggling to get the binding to work correctly on the sub elements. I already looked at this example, but I'm one level more deeply nested that this and so this solution didn't help: Data Binding in WPF User Controls

Here is my UserControl:

<UserControl x:Class="BindingTest.MyList"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             x:Name="uc">
    <Grid>
        <ListView Grid.Row="1" ItemsSource="{Binding Items, ElementName=uc}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Text}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</UserControl>

And the codebehind:

[ContentProperty("Items")]
public partial class MyList : UserControl {
    public MyList() {
        InitializeComponent();
    }

    public ObservableCollection<MyItem> Items { get; set; } = new ObservableCollection<MyItem>();
}

public class MyItem : DependencyObject {
    public string Text {
        get {
            return (string)this.GetValue(TextProperty);
        }
        set {
            this.SetValue(TextProperty, value);
        }
    }
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyItem), new PropertyMetadata());
}

And then how I'm using it in the app:

<Window x:Class="BindingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BindingTest"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TextBox Grid.Row="0" Text="{Binding Val1, Mode=TwoWay}" />

        <local:MyList Grid.Row="1">
            <local:MyItem Text="{Binding Val1}" />
            <local:MyItem Text="Two" />
            <local:MyItem Text="Three" />
        </local:MyList>

        <ListView Grid.Row="2">
            <ListViewItem Content="{Binding Val1}" />
            <ListViewItem Content="Bravo" />
            <ListViewItem Content="Charlie" />
        </ListView>
    </Grid>
</Window>

And finally my window:

public partial class MainWindow : Window, INotifyPropertyChanged {
    public MainWindow() {
        InitializeComponent();
    }

    string _val1 = "Foo";
    public string Val1 {
        get { return _val1; }
        set {
            _val1 = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Val1"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

The problem I'm encountering is that the first "MyItem" in the user control ends up displaying blank text, whereas the first ListViewItem in the stock control displays Foo as you would expect.

I understand this is because in the MyList control the DataTemplate switches the DataContext for each item in the collection, but no matter what I try in the binding expressions, either in the window or the user control, I can't get it to bind to Val1 on the window correctly.

What am I doing wrong?

Community
  • 1
  • 1
djm181
  • 106
  • 8

2 Answers2

0

You are correct that the DataContext for your MyItem object does not have a Val1 value to bind to and that's why the binding is breaking. This is the same problem that you commonly see in DataGrids, where the context for the internal item (MyItem) is set to an element of the ItemSource (in your case the ObservableCollection Items).

They way I have always got around the problem was to include an object in the resources of the DataGrid or Control that has a reference to the DataContext of the control. This way you can bind to that object as a StaticResource and you don't have to alter the XAML in the Control.

Class that holds the data context:

 public class BindingProxy : Freezable
{
    protected override Freezable CreateInstanceCore() { return new BindingProxy(); }

    public static readonly DependencyProperty ContextProperty =
        DependencyProperty.Register("Context", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));

    public object Context
    {
        get { return (object)GetValue(ContextProperty); }
        set { SetValue(ContextProperty, value); }
    }
}

Then add an instance of the BindingProxy to the resources of your MyList, and bind the Context property to the MyList's binding. Then you can bind to the BindingProxy as a StaticResource by name. The Path is to The Context property of the resource and then the normal path to the value you want to bind to.

<local:MyList Grid.Row="1">
        <local:MyList.Resources>
            <local:BindingProxy x:Key="VMProxy" Context="{Binding}"/>
        </local:MyList.Resources>
        <local:MyItem Text="{Binding Path=Context.Val1, Source={StaticResource VMProxy}}"/>
        <local:MyItem Text="Two" />
        <local:MyItem Text="Three" />
    </local:MyList>

I tested it with the control and window code you have and it worked like a charm. I can't take full credit for this solution, since I found it here on SO, but for the life of me I can't find the old answer that has this solution for DataGrids.

Lithium
  • 373
  • 7
  • 21
0

Actually, I found a way to make this work correctly. Instead of making a UserControl, I switched over to an ItemsControl, and changed the "MyItem" class to subclass ContentControl. Then the WPF takes care of setting the data context up for you. You can to specify a new template for the ItemsControl to avoid the Grid showing up as its own item in the list.

<ItemsControl x:Class="BindingTest.MyList"
                 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" 
                 mc:Ignorable="d" 
                 d:DesignHeight="300" d:DesignWidth="300"
                 x:Name="uc">
    <ItemsControl.Template>
        <ControlTemplate>
            <Grid>
                <ListView ItemsSource="{Binding Items, ElementName=uc}">
                    <ListView.Resources>
                        <Style TargetType="ListViewItem">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="ListViewItem">
                                        <Label Foreground="Blue" Content="{Binding Path=Text}" />
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </ListView.Resources>
                </ListView>
            </Grid>
        </ControlTemplate>
    </ItemsControl.Template>
</ItemsControl>

And:

public partial class MyList : ItemsControl {
    public MyList() {
        InitializeComponent();
    }
}

public class MyItem : ContentControl {
    public string Text {
        get {
            return (string)this.GetValue(TextProperty);
        }
        set {
            this.SetValue(TextProperty, value);
        }
    }
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyItem), new PropertyMetadata());
}
djm181
  • 106
  • 8