WPF Data binding doesnt work for custom controls that are defined inside a xaml collection tag. I just want to define a collection of custom widgets inside a custom control and bind some widgets properties against viewmodel properties. Like so.

<Window x:Class="WpfApp1.MainWindow"
        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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <local:MyCustomControl>
            <local:MyCustomControl.Widgets>
                <local:MyCustomWidget ImportantToggle="{Binding SomeToggle}"/>
            </local:MyCustomControl.Widgets>
        </local:MyCustomControl>
    </Grid>
</Window>

That is my custom control. I use an obseravblecollection for the widgets and call SetValue in the constructor to get propertychanged callback later (right now not used in example)

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;

namespace WpfApp1
{
    public class MyCustomControl : FrameworkElement
    {
        public ObservableCollection<MyCustomWidget> Widgets
        {
            get { return (ObservableCollection<MyCustomWidget>)this.GetValue(WidgetsProperty); }
            set { this.SetValue(WidgetsProperty, value); }
        }
        public static DependencyProperty WidgetsProperty = DependencyProperty.Register("Widgets", typeof(ObservableCollection<MyCustomWidget>), typeof(MyCustomControl), new PropertyMetadata(null, (e, args) => ((MyCustomControl)e).WidgetsChanged(args)));

        public void WidgetsChanged(DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine("widgets collection object changed inside my custom control!");
        }

        public MyCustomControl()
        {
            this.SetValue(WidgetsProperty, new ObservableCollection<MyCustomWidget>());
        }
    }
}

and that is my custom widget:

namespace WpfApp1
{
    public class MyCustomWidget : FrameworkContentElement
    {
        public bool ImportantToggle
        {
            get { return (bool)this.GetValue(ImportantToggleProperty); }
            set { this.SetValue(ImportantToggleProperty, value); }
        }

        public static DependencyProperty ImportantToggleProperty = DependencyProperty.Register("ImportantToggle", typeof(bool), typeof(MyCustomWidget), new PropertyMetadata(false, (e, args) => ((MyCustomWidget)e).ImportantToggleChanged(args)));

        public void ImportantToggleChanged(DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine("my toggle changed inside my custom widget!");
        }
    }
}

And finally my simplistic ViewModel:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApp1
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool _someToggle;
        public bool SomeToggle
        {
            get { return this._someToggle; }
            set
            {
                this._someToggle = value;
                this.NotifyPropertyChanged();
            }
        }

        public MainViewModel()
        {
            this.SomeToggle = !this.SomeToggle;
        }
    }
}

Thats the output I get from Debug.Writeline: widgets collection object changed inside my custom control!

Observation: I cant bind against properties of MyCustomWidget. I understand that the binding might fail in this scenario because the observablecollection is created inside of the constructor of mycustomcontrol, but I dont know how to fix it to get the binding working inside mycustomwidget.

1 Answers

2
Ed Plunkett On

For that binding to work, your local:MyCustomWidget needs to have the same DataContext as the main window. WPF elements inherit their logical parent's DataContext. MyCustomWidget doesn't, because it's not in the logical tree. It's just sitting there. You're not adding it to any kind of normal child collection of its parent, just to a random ObservableCollection that the framework doesn't know about.

The code below is probably a crude hack. I haven't investigated this corner of WPF. I urge you with the utmost sincerity to find out the right way of doing this. But with this addition to your code, I hit the propertychanged event in MyCustomWidget when the binding is initialized.

public MyCustomControl()
{
    this.SetValue(WidgetsProperty, new ObservableCollection<MyCustomWidget>());
    Widgets.CollectionChanged += Widgets_CollectionChanged;
}

private void Widgets_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems is System.Collections.IEnumerable)
    {
        foreach (MyCustomWidget widget in e.NewItems)
        {
            AddLogicalChild(widget);
        }
    }
}

By the way, you can save the trouble of toggling the toggle in the MainViewModel constructor. That happens long before the binding exists. I added a checkbox instead:

<StackPanel>
    <CheckBox IsChecked="{Binding SomeToggle}">Test Toggle</CheckBox>
    <local:MyCustomControl>
        <local:MyCustomControl.Widgets>
            <local:MyCustomWidget 
                ImportantToggle="{Binding SomeToggle}"
                />
        </local:MyCustomControl.Widgets>
    </local:MyCustomControl>
</StackPanel>

Update:

This omits your Widgets collection entirely, and the binding works without any effort on our part. The child widgets will be in MyCustomControl.Children. Importantly that we aren't limiting the child type to MyCustomWidget any more. That's a significant design change, and may not fit your requirements. You could examine the Panel class closely, and write a class that works the same way, but accepts only one type of child (that would mean writing an analog of UIElementCollection, which will be mostly a big pile of tedious boilerplate).

MyCustomControl.cs

[ContentProperty("Children")]
public class MyCustomControl : Panel
{
}

MyCustomWidget.cs

public class MyCustomWidget : Control
{
    public bool ImportantToggle
    {
        get { return (bool)this.GetValue(ImportantToggleProperty); }
        set { this.SetValue(ImportantToggleProperty, value); }
    }

    public static DependencyProperty ImportantToggleProperty = 
        DependencyProperty.Register("ImportantToggle", typeof(bool), typeof(MyCustomWidget), 
            new PropertyMetadata(false, (e, args) => ((MyCustomWidget)e).ImportantToggleChanged(args)));

    public void ImportantToggleChanged(DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("my toggle changed inside my custom widget!");
    }
}

MainWindow.xaml

<local:MyCustomControl>
    <local:MyCustomWidget 
        ImportantToggle="{Binding SomeToggle}"
        />
</local:MyCustomControl>