具有 ComboBox 绑定的 WPF DataGridTemplateColumn(MVVM 模式)

发布于 2024-11-29 20:38:32 字数 1886 浏览 3 评论 0原文

我对以下 WPF DataGrid+ComboBox 场景感到疯狂。

我有一组看起来像的课程;

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
}

现在我想要的结果是一个 DataGrid,它显示 House 类型的行列表,并且在其中一列中,是一个 ComboBox,它允许用户更改 House.HouseOwner< 的值/强>。

在这种情况下,网格的 DataContext 是 ViewModel.Houses,对于 ComboBox,我希望将 ItemsSource 绑定到 ViewModel.Owners。

这可能吗?我对此感到很困惑...我能做的最好的事情就是正确获取 ItemsSource 绑定,但是 ComboBox(在 DataGridTemplateColumn 内)没有在每行中显示 House.HouseOwner 的正确值。

注意:如果我从图片中取出 ComboBox 并将 TextBlock 放入 DataTemplate 中,我可以正确地看到每行的值,但是获取 ItemsSource 以及在选择中显示正确的值对我来说不起作用...

在我的代码中,我已将窗口上的 DataContext 设置为 ViewModel,在网格上,DataContext 设置为 ViewModel.Houses。对于除此组合框之外的所有内容,它都有效...

我的违规列的 XAML 看起来像;

<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>

希望在这个问题上得到一些帮助......不过似乎需要一点巫术......

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...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

妄想挽回 2024-12-06 20:38:32

正如 default.kramer 所说,您需要从 SelectedItemSelectedValue 的绑定中删除 RelativeSource,如下所示(请注意,您应该将 Mode=TwoWay 添加到您的绑定中,以便组合框中的更改反映在您的模型中)。

<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>

但是,与他所说的不同,您不必删除 SelectedValue 的绑定。事实上,如果您删除它,它将不起作用(SelectedValueSelectedValuePath都应该在这里设置,就像您所做的那样),因为这就是允许绑定的原因识别从组合框到 DataGrid 的 HouseOwner 属性的选择的机制。

SelectedValue/SelectedValuePath 组合非常有趣。 SelectedValuePath 告诉数据绑定当前选定的 Owner 对象的 ID 属性代表其SelectedValue 告诉它该值应该绑定到 HouseOwner.ID,它是 DataGrid 上选定的对象。

因此,如果删除这些绑定,数据绑定机制唯一知道的是“选择了哪个对象”,并在 ComboBox 中的所选项目与所选项目上的 HouseOwner 属性之间建立对应关系DataGrid 中的项目,它们必须是“相同的对象引用”。这意味着,例如,以下内容将不起作用:(

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"}}
                };

请注意,Houses 集合中的“HouseOwners”与 Owners 集合中的“HouseOwners”不同(新))。但是,以下可以工作:

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]}
                };

希望这有帮助:)

更新:在第二种情况下,您可以通过覆盖获得相同的结果,而无需使引用相同Owner 类上的 >Equals(当然,因为它首先用于比较对象)。 (感谢 @RJ Lohan 在下面的评论中指出这一点)

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)

耀眼的星火 2024-12-06 20:38:32

感谢大家的帮助 - 我终于弄清楚为什么我无法选择 ComboBox 项目 - 是由于我在使用 DataGridComboBoxColumn 时附加到单元格样式的鼠标预览事件处理程序。

为那件事打了自己一巴掌,感谢其他人的帮助。

另外,作为注释;这对我有用的唯一方法是额外的;

IsSynchronizedWithCurrentItem="False"

添加到组合框,否则它们由于某种原因都显示相同的值。

另外,我认为我的绑定中似乎不需要 SelectedValue/SelectedValuePath 属性,因为我已经覆盖了绑定的所有者类型中的 Equals

最后,我必须明确设置;

Mode=TwoWay, UpdateSourceTrigger=PropertyChanged

在绑定中,以便在 ComboBox 更改时将值写回到绑定项。

因此,绑定的最终(工作)XAML 如下所示;

    <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>

干杯!

rJ

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

‖放下 2024-12-06 20:38:32

这绝对是可能的,并且您使用 ItemsSourceAncestorType 绑定走在正确的轨道上。但我认为我发现了一些错误。

首先,您的 ItemsSource 应该绑定到 DataContext.Owners,而不是 DataContext.Houses,对吗?您希望视图模型的所有者集合显示在下拉列表中。因此,首先,更改 ItemsSource 并取出与选择相关的内容,如下所示:

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

现在对其进行测试并确保 ItemsSource 正常工作。在这部分起作用之前,不要尝试乱搞选择。

关于选择,我认为您应该仅绑定 SelectedItem - 而不是 SelectedValue。对于此绑定,您不需要需要 RelativeSource 绑定 - DataContext 将是单个 House,因此您可以直接绑定其 HouseOwner。我的猜测是:

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

最后,为了调试绑定,您可以 查看 Visual Studio 输出窗口 或使用 SnoopWPF 检查器。如果您计划进行大量 WPF,我建议尽早开始使用 Snoop。

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.

樱桃奶球 2024-12-06 20:38:32

基于 AbdouMoumen 的建议的完整示例。还删除了 SelectedValue 和选定的值路径。

输入图片此处描述

//---------
//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>

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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文