Search code examples
c#wpfbindingviewmodelitemscontrol

How can I bind properties of a view model to properties of items of a collection used as source for an items control


What I am trying to achieve is to bind properties of a ViewModel (mvvm light) object to some self made custom control in a grouped way.

So I created a CustomControl1, with a Title property and an ItemsCollection of my own custom type SomeDataControl.

     public class CustomControl1 : Control
    {
        static CustomControl1()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
        }

        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            "Text", typeof(string), typeof(CustomControl1), new PropertyMetadata(default(string)));

        public string Text
        {
            get { return (string) GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }



        public ObservableCollection<SomeDataControl> _ItemsCollection;
        public ObservableCollection<SomeDataControl> ItemsCollection {
            get
            {
                if(_ItemsCollection == null) _ItemsCollection = new ObservableCollection<SomeDataControl>();
                return _ItemsCollection;
            }
        }
    }

    public class SomeDataControl : DependencyObject
    {
        public static readonly DependencyProperty LAbelProperty = DependencyProperty.Register(
            "LAbel", typeof(string), typeof(SomeDataControl), new PropertyMetadata(default(string)));

        public string LAbel
        {
            get { return (string) GetValue(LAbelProperty); }
            set { SetValue(LAbelProperty, value); }
        }

        public static readonly DependencyProperty DValueProperty = DependencyProperty.Register(
            "DValue", typeof(double), typeof(SomeDataControl), new PropertyMetadata(default(double)));

        public double DValue
        {
            get { return (double) GetValue(DValueProperty); }
            set { SetValue(DValueProperty, value); }
        }
    }

I have also added a stlye to render the content of my control into an ItemsControl and bound the values to the appropriate fields like this:

<Style x:Key="ControlStyle" TargetType="local:CustomControl1">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <StackPanel>
                        <Label Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Text}"></Label>
                        <ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ItemsCollection}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <Label Content="{Binding Path=LAbel}" />
                                        <Label Content="{Binding Path=DValue}" />
                                    </StackPanel>

                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                    </StackPanel>

                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

And I put all this to my view with a view model as DataContext, so I can reach the values.

<Window x:Class="BindingTest.MainWindow" x:Name="thisControl" DataContext="{Binding Source={StaticResource Locator}, Path=VM}">
...
<local:CustomControl1 Text="{Binding Path=DataContext.Text, ElementName=thisControl}" Style="{StaticResource ControlStyle}" >
            <local:CustomControl1.ItemsCollection>
                <local:SomeDataControl LAbel="Apple" DValue="{Binding Path=DataContext.DVal1, Mode=TwoWay, ElementName=thisControl}">
                <local:SomeDataControl LAbel="Peach" DValue="{Binding Path=DataContext.DVal2, Mode=TwoWay, ElementName=thisControl}">
                <local:SomeDataControl LAbel="Pear" DValue="{Binding Path=DataContext.DVal3, Mode=TwoWay, ElementName=thisControl}"></local:SomeDataControl>

            </local:CustomControl1.ItemsCollection>
        </local:CustomControl1>

Everything goes fine, until I want to bind the DVal1,2 and 3 to the specific items. They are all with the default values. I have been looking for the answer for 3 days already, but I could not find anything that would help. I tried also using DependenyProperty for the collection, or changing the type of it to a simple list, of also Freezable, but nothing helped at all.

I would really like to declare my groups this way in XAML and not putting everything together in my ViewModel to reach the layout.

Any kind of help would be great.

Thank you in advance.


Solution

  • Actually I found the answer by using both of the tips and some googling. The main problem was, that the my SomeDataControl items were not part of the visual tree, therefore they did not get the datacontext of the higher level FrameworkElement. So I introduced a Binding Proxy thank to this post:

    Usage of Binding Proxy

    So my XAML looks like this:

    <Window.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Styles.xaml"></ResourceDictionary>
                </ResourceDictionary.MergedDictionaries>
                <local:BindingProxy x:Key="proxy" Data="{Binding}" />
            </ResourceDictionary>        
        </Window.Resources>
    
    ...
    
    <local:SomeDataControl LAbel="Apple" DValue="{Binding Path=Data.DVal1, Source={StaticResource proxy}}"></local:SomeDataControl>
    
    

    and the code for the binding proxy is also very good and easy, it worth to re-share it:

        public class BindingProxy : Freezable
        {
            #region Overrides of Freezable
    
            protected override Freezable CreateInstanceCore()
            {
                return new BindingProxy();
            }
    
            #endregion
    
            public object Data
            {
                get { return (object) GetValue(DataProperty); }
                set { SetValue(DataProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty DataProperty =
                DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
        }
    

    A little explanation also comes in the linked blog post

    The solution to our problem is actually quite simple, and takes advantage of the Freezable class. The primary purpose of this class is to define objects that have a modifiable and a read-only state, but the interesting feature in our case is that Freezable objects can inherit the DataContext even when they’re not in the visual or logical tree.

    Thank you all for your support.