WPF DataGridTemplateColumn with ComboBox Binding (MVVM pattern)
Asked Answered
D

4

14

I'm going bonkers with the following WPF DataGrid+ComboBox scenario.

I have a set of classes which look like;

class Owner
{
    int ID { get; }
    string Name { get; }

    public override ToString()
    { 
        return this.Name;
    }
}

class House
{
    int ID { get; }
    Owner HouseOwner { get; set; }
}

class ViewModel
{
    ObservableCollection<Owner> Owners;
    ObservableCollection<House> Houses
}

Now my desired outcome is a DataGrid which shows a list of rows of type House, and in one of the columns, is a ComboBox which allows the user to change the value of House.HouseOwner.

In this scenario, the DataContext for the grid is ViewModel.Houses and for the ComboBox, I want the ItemsSource to be bound to ViewModel.Owners.

Is this even possible? I'm going mental with this... the best I've been able to do is to correctly get the ItemsSource bound, however the ComboBox (inside a DataGridTemplateColumn) is not showing the correct values for House.HouseOwner in each row.

NOTE: If I take the ComboBox out of the picture and put a TextBlock in the DataTemplate instead, I can correctly see the values for each row, but getting both an ItemsSource as well as show the correct value in the selection is not working for me...

Inside my code behind, I have set the DataContext on the Window to ViewModel and on the grid, the DataContext is set to ViewModel.Houses. For everything except this combobox, it's working...

My XAML for the offending column looks like;

<DataGridTemplateColumn Header="HouseOwner">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ComboBox ItemsSource="{Binding Path=DataContext.Owners, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                        DisplayMemberPath="Name"
                        SelectedItem="{Binding HouseOwner, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
                        SelectedValue="{Binding HouseOwner.ID, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Mode=OneWay}"
                        SelectedValuePath="ID" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Would love some help on this one... seems like a bit of Voodoo is required though...

Deloisedelong answered 17/8, 2011 at 5:22 Comment(0)
C
16

as default.kramer said, you need to remove the RelativeSource from your bindings for the SelectedItem and SelectedValue like this (notice that you should add Mode=TwoWay to your binding so that the change in the combobox is reflected in your model).

<DataGridTemplateColumn Header="House Owner">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ComboBox
                ItemsSource="{Binding Path=DataContext.Owners, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                DisplayMemberPath="Name"
                SelectedItem="{Binding HouseOwner, Mode=TwoWay}"
                SelectedValue="{Binding HouseOwner.ID}"
                SelectedValuePath="ID"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

However, unlike he said, you don't have to remove the binding for the SelectedValue. In fact, if you remove it, it won't work (both SelectedValue and SelectedValuePath should be set here, as you've done), because that's what's allowing the binding mechanism to identify the selection from the combobox to the DataGrid's HouseOwner property.

SelectedValue/SelectedValuePath combination is very interesting. SelectedValuePath tells the databinding that the ID property of the Owner object currently selected represents its value, SelectedValue tells it that that value should be bound to the HouseOwner.ID which is the selected object on the DataGrid.

Therefore, if you remove those binding, the only thing the databinding mechanism will know is "what object is selected", and to make the correspondence between the selected item in the ComboBox and the HouseOwner property on the selected item in the DataGrid, they have to be "the same object reference". Meaning that, for example, the following wouldn't work:

Owners = new ObservableCollection<Owner>
                {
                    new Owner {ID = 1, Name = "Abdou"},
                    new Owner {ID = 2, Name = "Moumen"}
                };
Houses = new ObservableCollection<House>
                {
                    new House {ID = 1, HouseOwner = new Owner {ID = 1, Name = "Abdou" }},
                    new House {ID = 2, HouseOwner = new Owner {ID = 2, Name = "Moumen"}}
                };

(notice that the "HouseOwners" of the Houses collection are different (new) from the ones in the Owners collection). However, the following would work:

Owners = new ObservableCollection<Owner>
                {
                    new Owner {ID = 1, Name = "Abdou"},
                    new Owner {ID = 2, Name = "Moumen"}
                };
Houses = new ObservableCollection<House>
                {
                    new House {ID = 1, HouseOwner = Owners[0]},
                    new House {ID = 2, HouseOwner = Owners[1]}
                };

Hope this helps :)

Update: in the second case, you can get the same result without having the references being the same by overriding Equals on the Owner class (naturally, since it's used to compare the objects in the first place). (thanks to @RJ Lohan for noting this in the comments below)

Cymbiform answered 17/8, 2011 at 7:26 Comment(6)
Good explanation - I assumed that a House's Owner would be the same object reference as the Viewmodel's Owner, as shown in the second scenario.Surmount
Honestly, I also thought that was the problem, until I tried it myself :)Cymbiform
Thanks for the reply, but this is still not quite working for me. I have the XAML binding setup just as you note, with one change - because Owner.ID has only a getter, I have to set the SelectedValue binding to OneWay, whilst the SelectedItem binding is set as TwoWay (else I get runtime exceptions). Also, I thought TwoWay binding was the default, so wouldn't need to be specified on the SelectedItem? Anyway, still not working for me - I am still unable to change the selection in the ComboBox...Deloisedelong
Another thought; RE: "to make the correspondence between the selected item in the ComboBox and the HouseOwner property on the selected item in the DataGrid, they have to be "the same object reference"... I have overridden the Equals method in my Owner object (in the real code) to equate on ID, so I thought that the value bindings would become redundant, as the SelectedItem binding could compare different instances of Owner where the IDs are the same. Incidentally, I have managed to get this working in code-behind with a DataGridComboBoxColumn... the XAML approach still eludes meDeloisedelong
The default mode for the Binding is OneWay, and forgetting to set it to TwoWay gave me a lot of headaches. For your remark on the "same object reference", you're right. I just tried it out, and overriding the Equals does work (that's to be expected, since that is the method that is used to check the equality, and by default it checks if it's the "same reference" ;) ). Glad to help anyway :)Cymbiform
@Cymbiform ., when I try to get the value for the comboBox it gives out value like "System.Data.DataRowView"Gerhard
D
10

Thanks for the help all - I finally worked out why I couldn't select the ComboBox items - was due to a mouse preview event handler I had attached to the cell style when I was using a DataGridComboBoxColumn.

Have slapped myself for that one, thanks for the other assistance.

Also, as a note; the only way this will work for me is with an additional;

IsSynchronizedWithCurrentItem="False"

Added to the ComboBox, else they all show the same value for some reason.

Also, I don't appear to require the SelectedValue/SelectedValuePath properties in my Binding, I believe because I have overridden Equals in my bound Owner type.

And lastly, I have to explicitly set;

Mode=TwoWay, UpdateSourceTrigger=PropertyChanged

In the Binding in order for the values to be written back to the bound items when the ComboBox has changed.

So, the final (working) XAML for the binding looks like this;

    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ComboBox 
                ItemsSource="{Binding Path=DataContext.Owners,  
                RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                IsSynchronizedWithCurrentItem="False"
                SelectedItem="{Binding HouseOwner, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"  />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>

Cheers!

rJ

Deloisedelong answered 18/8, 2011 at 0:3 Comment(1)
The IsSynchronizedWithCurrentItem property not being set to false caused the same symptoms for me too, binding comboboxes within a HeaderedItemsControl. Ta!Generosity
S
3

This is definitely possible and you're on the right track using an AncestorType binding for the ItemsSource. But I think I see a couple of mistakes.

First, your ItemsSource should be binding to DataContext.Owners, not DataContext.Houses, correct? You want the viewmodels' collection of Owners to show up in the drop-down. So first, change the ItemsSource and take out the Selection-related stuff, like this:

<ComboBox ItemsSource="{Binding Path=DataContext.Owners, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
          DisplayMemberPath="Name" />

Now test it out and make sure the ItemsSource is working correctly. Don't try messing around with selection until this part works.

Regarding selection, I think you should be binding SelectedItem only - not SelectedValue. For this binding, you do not want a RelativeSource binding - the DataContext will be a single House so you can bind directly its HouseOwner. My guess is this:

<ComboBox ItemsSource="{Binding Path=DataContext.Owners, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
          DisplayMemberPath="Name"
          SelectedItem="{Binding HouseOwner}" />

Finally, for debugging bindings you can see the Visual Studio Output window or step up to a tool like Snoop or WPF Inspector. If you plan on doing a lot of WPF, I would recommend getting started with Snoop sooner than later.

Surmount answered 17/8, 2011 at 5:53 Comment(2)
Thanks for taking a look. You're right about the first point; ItemsSource should be DataContext.Owners: a typo in my question. I was trying to cut the code down to the relevant pieces and mis-typed that.Deloisedelong
I've fiddled with my code a bit, and now I have hit a slightly different snag. With the code implemented as you have, with only SelectedItem bound, all the rows show the same value for HouseOwner... no idea what that means, but it appears they are not correctly bound. Whilst googling, I came across another suggestion where I have added; IsSynchronizedWithCurrentItem="False" To my ComboBox. This has resulted in the correct values being shown for each item, and the correct choices in the dropdown... however now I am unable to change the selection!Deloisedelong
L
0

Full example based on AbdouMoumen's suggestion. Also removed SelectedValue & SelectedValuePath.

enter image description here

//---------
//CLASS STRUCTURES.    
//---------
//One grid row per house.    
public class House
{
    public string name { get; set; }
    public Owner ownerObj { get; set; }
}

//Owner is a combobox choice.  Each house is assigned an owner.    
public class Owner
{
    public int id { get; set; }
    public string name { get; set; }
}

//---------
//FOR XAML BINDING.    
//---------
//Records for datagrid.  
public ObservableCollection<House> houses { get; set; }

//List of owners.  Each house record gets an owner object assigned.    
public ObservableCollection<Owner> owners { get; set; }

//---------
//INSIDE “AFTER CONTROL LOADED” METHOD.  
//---------
//Populate list of owners.  For combobox choices.  
owners = new ObservableCollection<Owner>
{
    new Owner {id = 1, name = "owner 1"},
    new Owner {id = 2, name = "owner 2"}
};

//Populate list of houses.  Again, each house is a datagrid record.  
houses = new ObservableCollection<House>
{
    new House {name = "house 1", ownerObj = owners[0]},
    new House {name = "house 2", ownerObj = owners[1]}
};


<DataGrid ItemsSource="{Binding Path=houses, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" >
    <DataGrid.Columns>
        <DataGridTextColumn Header="name" Binding="{Binding name}" />
        <DataGridTextColumn Header="owner (as value)" Binding="{Binding ownerObj.name}"/>

        <DataGridTemplateColumn Header="owner (as combobox)" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ComboBox
                            ItemsSource="{Binding Path=owners, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                            DisplayMemberPath="name"
                            SelectedItem="{Binding ownerObj, Mode=TwoWay}"
                            />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>

</DataGrid>
Lenoir answered 20/8, 2017 at 0:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.