Search code examples
c#wpfuser-controlsdependency-propertiesdatagridcomboboxcolumn

How to bind dependency property to DataGridComboBoxColumn in UserControl?


The XAML of my UserControl with name "Clinical_Protocol":

<Grid>
        <DataGrid Name="ClinicalProtocolDataGrid"
                  ItemsSource="{Binding DataGridItems, ElementName=Clinical_Protocol}"
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridComboBoxColumn Header="Structure ID"
                                        ItemsSource="{Binding ComboBoxItems, ElementName=Clinical_Protocol, Mode=TwoWay}"
                                        SelectedItemBinding="{Binding SelectedStructureId, Mode=TwoWay}"
                                        />
                <DataGridTextColumn Header="RT ROI Type Code" 
                                    Binding="{Binding RtRoiInterpretedTypeCode}"/>
                <DataGridTextColumn Header="RT ROI Type Description"
                                    Binding="{Binding RtRoiInterpretedTypeDescription}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>

The dependency property for ItemsSource of the DataGrid in the code behind:

internal ObservableCollection<ClinicalProtocolDataGridItem> DataGridItems
{
    get { return (ObservableCollection<ClinicalProtocolDataGridItem>)GetValue(DataGridItemsProperty); }
    set { SetValue(DataGridItemsProperty, value); }
}

internal static readonly DependencyProperty DataGridItemsProperty =
            DependencyProperty.Register("DataGridItems", typeof(ObservableCollection<ClinicalProtocolDataGridItem>),
                typeof(ClinicalProtocolView), new PropertyMetadata(null, DataGridItemsPropertyChangedCallback));

private static void DataGridItemsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var control = (ClinicalProtocolView)d;
    control.DataGridItems = (ObservableCollection<ClinicalProtocolDataGridItem>)e.NewValue;
}

The according ClinicalProtocolDataGridItem class:

public class ClinicalProtocolDataGridItem
{
    public string RtRoiInterpretedTypeCode { get; }
    public string RtRoiInterpretedTypeDescription { get; }
    public object SelectedStructureId { get; set; }

    public ClinicalProtocolDataGridItem(string rtRoiInterpretedTypeCode, string rtRoiInterpretedTypeDescription)
    {
        RtRoiInterpretedTypeCode = rtRoiInterpretedTypeCode ??
                                       throw new ArgumentNullException(nameof(rtRoiInterpretedTypeCode));
        RtRoiInterpretedTypeDescription = rtRoiInterpretedTypeDescription ??
                                              throw new ArgumentNullException(nameof(rtRoiInterpretedTypeDescription));
    }
}

And finally the dependency property ComboBoxItems:

public ObservableCollection<ComboBoxItem> ComboBoxItems
{
    get { return (ObservableCollection<ComboBoxItem>)GetValue(ComboBoxItemsProperty); }
    set { SetValue(ComboBoxItemsProperty, value); }
}

public static readonly DependencyProperty ComboBoxItemsProperty =
    DependencyProperty.Register("ComboBoxItems", typeof(ObservableCollection<ComboBoxItem>),
        typeof(ClinicalProtocolView), new PropertyMetadata(new ObservableCollection<ComboBoxItem>(),
                    PropertyChangedCallback));

private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var control = (ClinicalProtocolView)d;
    control.ComboBoxItems = (ObservableCollection<ComboBoxItem>)e.NewValue;
}

I try to populate both dependency properties with the following:

private void AddSomeComboBoxItems()
{
    ComboBoxItems = new ObservableCollection<ComboBoxItem>
    {
        new ComboBoxItem(){Content = "S1",  HorizontalContentAlignment = HorizontalAlignment.Center, VerticalContentAlignment = VerticalAlignment.Center},
        new ComboBoxItem(){Content = "S2",  HorizontalContentAlignment = HorizontalAlignment.Center, VerticalContentAlignment = VerticalAlignment.Center},
        new ComboBoxItem(){Content = "S3",  HorizontalContentAlignment = HorizontalAlignment.Center, VerticalContentAlignment = VerticalAlignment.Center},
}

private void AddSomeRows()
{
    var items = new List<ClinicalProtocolDataGridItem>
    {
        new ClinicalProtocolDataGridItem("PTV", "Description of PTV, and it is a very long description!"),
        new ClinicalProtocolDataGridItem("OAR", "Description of OAR, and it is a very long description!"),
    };

    DataGridItems = new ObservableCollection<ClinicalProtocolDataGridItem>(items);
}

When I import it into a WPF application I get the following:

Combo Box Example picture

And the error message: DataGridComboBoxColumn.ItemsSource IEnumerable Cannot find governing FrameworkElement or FrameworkContentElement for target element. Error message

Microsoft documentation says that it is possible to bind a collection of ComboBoxItem. What am I missing? I don't want to have a static resource, because the combo box items might change during runtime.


Solution

  • The DataGridColumn is a simple DependencyObject, not a UI element. It is not embedded in the visual tree and cannot get values from it. The ElementName and FindAncestor bindings will not work in it.

    You need to get the collection into a static resource and only then get it from there for the DataGridColumn.

    <DataGrid Name="ClinicalProtocolDataGrid"
              ItemsSource="{Binding DataGridItems, ElementName=Clinical_Protocol}"
              AutoGenerateColumns="False">
        <FrameworkElement.Resources>
            <!--Using a CollectionViewSource as a proxy to create a "static link".-->
            <CollectionViewSource
                x:Key="comboBoxItems"
                Source="{Binding ComboBoxItems, ElementName=Clinical_Protocol}"/>
        </FrameworkElement.Resources>
        <DataGrid.Columns>
            <DataGridComboBoxColumn
                Header="Structure ID"
                ItemsSource="{Binding Source={StaticResource comboBoxItems}}"
                SelectedItemBinding="{Binding SelectedStructureId, Mode=TwoWay}"/>
    

    P.S. Please note my comment on your question. I explain there why even after a successful binding to such a source, the ComboBoxs may still work incorrectly.