views:

430

answers:

4

Hi,

My question: How do I bind the SelectedItem from a primary datagrid to the ItemsSource for a secondary datagrid?

In detail: I have two datagrids on my view. The first shows a collection of teams and the second shows as list of people in the selected team.

When I select a team from the grid I can see that the SelectedTeam property is getting updated correctly, but the People grid is not getting populated.

Note: I am not able to use nested grids, or the cool master-detail features provided in the SL data-grid.

UPDATE: Replacing the parent datagrid with a ComboBox gives completely different results and works perfectly. Why would ComboBox.SelectedItem and DataGrid.SelectedItem behave so differently?

Thanks,
Mark


Simple Repro:

VIEW:

<UserControl x:Class="NestedDataGrid.MainPage"
             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"
             mc:Ignorable="d"
             xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">
    <StackPanel x:Name="LayoutRoot">
        <TextBlock Text="Teams:" />
        <data:DataGrid ItemsSource="{Binding Teams}"
                       SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
                       AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Id" Binding="{Binding TeamId}" />
                <data:DataGridTextColumn Header="Desc" Binding="{Binding TeamDesc}" />
            </data:DataGrid.Columns>
        </data:DataGrid>
        <TextBlock Text="Peeps:" />
        <data:DataGrid ItemsSource="{Binding SelectedTeam.People}"
                       AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Id"
                                         Binding="{Binding PersonId}" />
                <data:DataGridTextColumn Header="Name"
                                         Binding="{Binding Name}" />
            </data:DataGrid.Columns>
        </data:DataGrid>
    </StackPanel>
</UserControl>


CODE_BEHIND:

using System.Windows.Controls;
namespace NestedDataGrid
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            this.LayoutRoot.DataContext = new ViewModel();
        }

    }
}


VIEWMODEL:

using System.Collections.ObjectModel;
namespace NestedDataGrid
{
    public class ViewModel: ObjectBase
    {
        public ViewModel()
        {
            ObservableCollection<Person> RainbowPeeps = new ObservableCollection<Person>()
            {
                new Person(){ PersonId=1, Name="George"}, 
                new Person(){ PersonId=2, Name="Zippy"}, 
                new Person(){ PersonId=3, Name="Bungle"}, 
            };

            ObservableCollection<Person> Simpsons = new ObservableCollection<Person>()
            {
                new Person(){ PersonId=4, Name="Moe"}, 
                new Person(){ PersonId=5, Name="Barney"}, 
                new Person(){ PersonId=6, Name="Selma"}, 
            };

            ObservableCollection<Person> FamilyGuyKids = new ObservableCollection<Person>()
            {
                new Person(){ PersonId=7, Name="Stewie"}, 
                new Person(){ PersonId=8, Name="Meg"}, 
                new Person(){ PersonId=9, Name="Chris"}, 
            };


            Teams = new ObservableCollection<Team>()
            {
                new Team(){ TeamId=1, TeamDesc="Rainbow", People=RainbowPeeps},
                new Team(){ TeamId=2, TeamDesc="Simpsons", People=Simpsons},
                new Team(){ TeamId=3, TeamDesc="Family Guys", People=FamilyGuyKids },
            };
        }


        private ObservableCollection<Team> _teams;
        public ObservableCollection<Team> Teams
        {
            get { return _teams; }
            set
            {
                SetValue(ref _teams, value, "Teams");
            }
        }


        private Team _selectedTeam;
        public Team SelectedTeam
        {
            get { return _selectedTeam; }
            set
            {
                SetValue(ref _selectedTeam, value, "SelectedTeam");
            }
        }


    }
}


ASSOCIATED CLASSES:

using System;
using System.ComponentModel;

namespace NestedDataGrid
{
    public abstract class ObjectBase : Object, INotifyPropertyChanged
    {
        public ObjectBase()
        { }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void _OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler pceh = PropertyChanged;
            if (pceh != null)
            {
                pceh(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected virtual bool SetValue<T>(ref T target, T value, string propertyName)
        {
            if (Object.Equals(target, value))
            {
                return false;
            }

            target = value;
            _OnPropertyChanged(propertyName);

            return true;
        }

    }


    public class Person: ObjectBase
    {
        private int  _personId;
        public int PersonId
        {
            get { return _personId; }
            set
            {
                SetValue(ref _personId, value, "PersonId");
            }
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                SetValue(ref _name, value, "Name");
            }
        }

    }

    public class Team : ObjectBase
    {

        private int _teamId;
        public int TeamId
        {
            get { return _teamId; }
            set
            {
                SetValue(ref _teamId, value, "TeamId");
            }
        }

        private string _teamDesc;
        public string TeamDesc
        {
            get { return _teamDesc; }
            set
            {
                SetValue(ref _teamDesc, value, "TeamDesc");
            }
        }


        private ObservableCollection<Person> _people;
        public ObservableCollection<Person> People
        {
            get { return _people; }
            set
            {
                SetValue(ref _people, value, "People");
            }
        }

    }
}


UPDATE

Replacing the first datagrid with a combobox and eveything works OK. Why would DataGrid.SelectedItem and ComboBox.SelectedItem behave so differently?

<StackPanel x:Name="LayoutRoot">        
    <TextBlock Text="Teams:" />
    <ComboBox SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
                    ItemsSource="{Binding Teams}"/>
    <TextBlock Text="{Binding SelectedTeam}" />    
    <TextBlock Text="Peeps:" />
    <data:DataGrid ItemsSource="{Binding SelectedTeam.People}" />    
</StackPanel>
+1  A: 

Having done some tests.

First I just wanted to confirm that the Binding itself is working. It works quite happly when the second DataGrid is swapped out for a ListBox. I've gone so far to confirm that the second DataGrid is having its ItemsSource property changed by the binding engine.

I've also swapped out the first DataGrid for a ListBox and then the second DataGrid starts working quite happly.

In addition if you wire up the SelectionChanged event on the first datagrid and use code to assign directly to the second datagrid it starts working.

I've also removed the SelectedItem binding on the first Grid and set up an ElementToElement bind to it from the on the ItemsSource property of the second Grid. Still no joy.

Hence the problem is narrowed down to SelectedItem on one DatGrid to the ItemsSource of another via the framework binding engine.

Reflector provides a possible clue. The Data namespace contains an Extensions static class targeting DependencyObject which has an AreHandlersSuspended method backed bye a static variable. The which the code handling changes to the ItemsSource property uses this method and does nothing if it returns true.

My unconfirmed suspicion is that in the process of the first Grid assigning its SelectedItem property it has turned on the flag in order to avoid an infinite loop. However since this flag is effectively global any other legitmate code running as a result of this SelectedItem assignment is not being executed.

Anyone got SL4 and fancy testing on that?
Any MSFTers lurking want to look into?

If SL4 still has it this will need reporting to Connect as a bug.

AnthonyWJones
+1 Thanks Anthony. I also got examples working with ListBox, and can probably use that for this particular scenario as my "parent" grid is very simple. I will leave this question open hoping that someone from MS can take a look at this.
Mark Cooper
I'm hoping the someone with a Beta SL4 will come along and give your repro a go on that, I'm too busy generating production SL3 to be mucking about Betas right now but there is no point raising a bug for this if its fixed already. I can certainly see my own code needing a fix for this at some point.
AnthonyWJones
Hi anthony, Thought you might like to know that I found a much more elegant (although still more-awkward-than-it-should-be) solution using a custom CommandBehaviour. Have posted details below. HTH, Mark
Mark Cooper
A: 

I have a work-around. It involves a bit of code behind, so won't be favoured by purist MVVM zealots! ;-)

<StackPanel x:Name="LayoutRoot">        
    <TextBlock Text="Teams:" />
    <data:DataGrid x:Name="dgTeams"
                   SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
                   ItemsSource="{Binding Teams}" />
    <TextBlock Text="{Binding SelectedTeam}" />
    <TextBlock Text="Peeps:" />
    <data:DataGrid x:Name="dgPeeps" />
</StackPanel>

Code Behind:

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
        this.LayoutRoot.DataContext = new ViewModel();

        dgTeams.MouseLeftButtonUp += new MouseButtonEventHandler(dgTeams_MouseLeftButtonUp)
    }

    void dgTeams_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        DataGridRow row = DependencyObjectHelper.FindParentOfType<DataGridRow>(e.OriginalSource as DependencyObject);

        ///get the data object of the row
        if (row != null && row.DataContext is Team)
        {
            dgPeeps.ItemsSource = (row.DataContext as Team).People;
        }
    }

}

The FindParentOfType method is detailed here: http://thoughtjelly.blogspot.com/2009/09/walking-xaml-visualtree-to-find-parent.html.

HTH Someone else,
Mark

Mark Cooper
+1  A: 

A better solution is to use add DataGridRowSelected command. This fits the MVVM pattern a whole lot better than my previous mouse click example.

This was inspired by some code from John Papa, I have created a detailed post about this http://thoughtjelly.blogspot.com/2009/12/binding-selecteditem-to-itemssource.html.

[Sits back contented and lights a cigar]
Mark

Mark Cooper
+1  A: 

I had the same problem, and "fixed" it by adding this to my code-behind.

Code behind:

    private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (_model != null)
        {
            _model.RefreshDetail();
        }
    }

Model:

    public void RefreshDetail()
    {
        RaisePropertyChanged("Detail");
    }
David Bridges