Search code examples
c#wpfdata-bindingdatagriduser-controls

SelectedValue binding of a nested DataGrid doesn't work while ItemsSource does


I have a DataGrid inside of a UserControl which in turn lies inside of another UserControl. This is due to other needs of the project and I can't change this nested architecture. I'm binding a list of Person class to this DataGrid. This is a dumbed-down version without using a VM, but in my real project I am using a VM.

My UserControl with the DataGrid:

<Grid>
    <DataGrid x:Name="MyDg"
        ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=local:UCDataGrid}, UpdateSourceTrigger=PropertyChanged}"
        MouseDoubleClick="MyDg_MouseDoubleClick"
        SelectedValue="{Binding SelectedValue, RelativeSource={RelativeSource AncestorType=local:UCDataGrid}, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>

Code Behind:

public partial class UCDataGrid : UserControl
{
    public event RoutedEventHandler RoutedDataGridDoubleClick;

    public UCDataGrid()
    {
        InitializeComponent();
    }

    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(object), typeof(UCDataGrid), new PropertyMetadata(null));
    public object ItemsSource
    {
        get { return GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public static readonly DependencyProperty SelectedValueProperty = DependencyProperty.Register("SelectedValue", typeof(object), typeof(UCDataGrid), new PropertyMetadata(null));
    public object SelectedValue
    {
        get { return GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }

    private void MyDg_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        RoutedDataGridDoubleClick?.Invoke(this, new RoutedEventArgs());
    }
}

2nd UserControl that contains the above control:

<Grid>
    <ContentControl Content="{Binding MyDataGrid, ElementName=ucDisplay}"/>
</Grid>

ucDisplay is simply the Name property value of this UserControl.

Code Behind:

Nothing fancy here.

public partial class UCDisplay : UserControl
{
    public UCDisplay()
    {
        InitializeComponent();
    }

    public static readonly DependencyProperty MyDataGridProperty = DependencyProperty.Register("MyDataGrid", typeof(object), typeof(UCDisplay), new PropertyMetadata(null));
    public object MyDataGrid
    {
        get { return GetValue(MyDataGridProperty); }
        set { SetValue(MyDataGridProperty, value); }
    }
}

Main Window

In my Main Window, I bind my People list as well as SelectedPerson instance, like so:

<Grid>
    <local:UCDisplay>
        <local:UCDisplay.MyDataGrid>
            <local:UCDataGrid ItemsSource="{Binding People}"
                              SelectedValue="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged}"
                              RoutedDataGridDoubleClick="UCDataGrid_RoutedDataGridDoubleClick"/>
        </local:UCDisplay.MyDataGrid>
    </local:UCDisplay>
</Grid>

Code Behind:

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    private List<Person> people;
    public List<Person> People
    {
        get => people;
        set => SetField(ref people, value);
    }

    private Person selectedPerson;
    public Person SelectedPerson
    {
        get => selectedPerson;
        set => SetField(ref selectedPerson, value);
    }

    public MainWindow()
    {
        InitializeComponent();
        People = GetPeople();
        DataContext = this;
    }

    private void UCDataGrid_RoutedDataGridDoubleClick(object sender, RoutedEventArgs e)
    {

    }

    private List<Person> GetPeople()
    {
        return new List<Person>
        {
            new Person() { Name = "A" },
            new Person() { Name = "B" },
            new Person() { Name = "C" }
        };
    }

    public class Person
    {
        public string Name { get; set; }
    }
}

Again, in reality I'm using a VM, this is only to keep things simple.

Now when I run this I can display my list content just fine. But when I double-click an item in my DataGrid, in the corresponding in my Main Window code behind, the SelectedPerson remains null, although its binding is identical to the People list. I confirm this by using a break point in the main code behind:

enter image description here

But if I debug and see the value in the code behind of my innermost UserControl, you see that the SelectedValue there has the correct selected items value.

enter image description here

So what am I doing wrong here? Why can't I seem to bind the SelectedValue although I do it exactly the same as my ItemsSource binding, but the latter works?


Solution

  • SelectedValue is supposed to be used in conjunction with SelectedValuePath. You should use SelectedItem instead.

    Besides that, you are missing a TwoWay Binding. Either explicitly declare the SelectedItem Binding TwoWay

    <DataGrid x:Name="MyDg"
        ItemsSource="{Binding ItemsSource,
            RelativeSource={RelativeSource AncestorType=UserControl}}"
        SelectedItem="{Binding SelectedItem,
            RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay}"/>
    

    or register the property to bind TwoWay by default:

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register(
            nameof(SelectedItem), typeof(object), typeof(UCDataGrid),
            new FrameworkPropertyMetadata(
                null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    
    public object SelectedItem
    {
        get { return GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }
    

    Also note that setting UpdateSourceTrigger=PropertyChanged is pointless in all your Bindings.