在WPF DataGrid中绑定ComboBoxColumn的ItemsSource

93

我有两个简单的Model类和一个ViewModel...

public class GridItem
{
    public string Name { get; set; }
    public int CompanyID { get; set; }
}

public class CompanyItem
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class ViewModel
{
    public ViewModel()
    {
        GridItems = new ObservableCollection<GridItem>() {
            new GridItem() { Name = "Jim", CompanyID = 1 } };

        CompanyItems = new ObservableCollection<CompanyItem>() {
            new CompanyItem() { ID = 1, Name = "Company 1" },
            new CompanyItem() { ID = 2, Name = "Company 2" } };
    }

    public ObservableCollection<GridItem> GridItems { get; set; }
    public ObservableCollection<CompanyItem> CompanyItems { get; set; }
}

...以及一个简单的窗口:

<Window x:Class="DataGridComboBoxColumnApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GridItems}" >
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" />
                <DataGridComboBoxColumn ItemsSource="{Binding CompanyItems}"
                                    DisplayMemberPath="Name"
                                    SelectedValuePath="ID"
                                    SelectedValueBinding="{Binding CompanyID}" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

在App.xaml.cs中,将ViewModel设置为MainWindow的DataContext

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        ViewModel viewModel = new ViewModel();

        window.DataContext = viewModel;
        window.Show();
    }
}

如您所见,我将DataGrid的ItemsSource设置为ViewModel的GridItems集合。 这部分是有效的,单个名称为"Jim"的网格行列被显示出来。

我还想将每一行中ComboBox的ItemsSource设置为ViewModel的CompanyItems集合,但这部分不起作用:ComboBox仍然为空,并且在调试器输出窗口中我看到了一个错误消息:

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=CompanyItems; DataItem=null; target element is 'DataGridComboBoxColumn' (HashCode=28633162); target property is 'ItemsSource' (type 'IEnumerable')

我认为WPF希望CompanyItemsGridItem的属性,但实际上并非如此,这就是绑定失败的原因。

我已经尝试使用RelativeSourceAncestorType进行工作,如下所示:

<DataGridComboBoxColumn ItemsSource="{Binding CompanyItems, 
    RelativeSource={RelativeSource Mode=FindAncestor,
                                   AncestorType={x:Type Window}}}"
                        DisplayMemberPath="Name"
                        SelectedValuePath="ID"
                        SelectedValueBinding="{Binding CompanyID}" />

但这会在调试器输出中给我另一个错误:

System.Windows.Data Error: 4 : 找不到绑定的源,引用为'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''。 BindingExpression:Path=CompanyItems; DataItem=null; target element is 'DataGridComboBoxColumn' (HashCode=1150788); target property is 'ItemsSource' (type 'IEnumerable')

问题:如何将DataGridComboBoxColumn的ItemsSource绑定到ViewModel的CompanyItems集合?是否可能?

提前感谢您的帮助!


各位,请不要让社区为您编写代码。发布一般性问题以获得反馈和解决方案选项。这些答案很容易适应许多情况。编写您的代码的答案并不是很有帮助。 - Jim Broiles
8个回答

139

请检查下面的 DataGridComboBoxColumn XAML 是否适合您的需求:

<DataGridComboBoxColumn 
    SelectedValueBinding="{Binding CompanyID}" 
    DisplayMemberPath="Name" 
    SelectedValuePath="ID">

    <DataGridComboBoxColumn.ElementStyle>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="ItemsSource" Value="{Binding Path=DataContext.CompanyItems, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </Style>
    </DataGridComboBoxColumn.ElementStyle>
    <DataGridComboBoxColumn.EditingElementStyle>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="ItemsSource" Value="{Binding Path=DataContext.CompanyItems, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
        </Style>
    </DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>

在这里,您可以找到解决您面临问题的另一种方案:使用组合框与WPF DataGrid


6
太好了,这个有效!只是我不明白为什么?为什么不能使用Rachel建议的更改后的原始代码?无论如何,非常感谢! - Slauma
1
我相信你可以在这里找到解释:http://wpf.codeplex.com/workitem/8153?ProjectName=wpf(请查看评论)。 - serge_gubenko
1
他们似乎已经决定把这个 bug (“我们在内部数据库中提交了一个 bug,将在未来的版本中修复。”)变成一个功能。请看看我在这个线程中的答案:问题已经通过文档解决,这强烈表明它永远不会改变。 - Slauma
1
+1 给 http://joemorrison.org/blog/2009/02/17/excedrin-headache-35401281-using-combo-boxes-with-the-wpf-datagrid/ 这个链接。它解决了我的问题。很遗憾,我花了大约5个小时才意识到我在项目中已经有这种类型用于我们正在做的其他事情:( 这总是一个学习过程。 - TravisWhidden
请在CodePlex上查看此链接:https://wpf.codeplex.com/workitem/8153 并投票支持此错误修复。 - juFo
显示剩余4条评论

50

MSDN上关于的ItemsSource文档中指出,只有静态资源、静态代码或内联集合的组合框项目可以绑定到ItemsSource:

要填充下拉列表,请先使用以下选项之一设置ComboBox的ItemsSource属性:

  • 一个静态资源。有关更多信息,请参见StaticResource标记扩展。
  • x:Static代码实体。有关更多信息,请参见x:Static标记扩展。
  • 一个ComboBoxItem类型的内联集合。

如果我理解正确,那么将DataContext的属性绑定到它是不可能的。

确实如此:当我在ViewModel中将CompanyItems设置为静态属性时...

public static ObservableCollection<CompanyItem> CompanyItems { get; set; }

...将ViewModel所在的命名空间添加到窗口中...

xmlns:vm="clr-namespace:DataGridComboBoxColumnApp"

...并将绑定更改为...

<DataGridComboBoxColumn
    ItemsSource="{Binding Source={x:Static vm:ViewModel.CompanyItems}}" 
    DisplayMemberPath="Name"
    SelectedValuePath="ID"
    SelectedValueBinding="{Binding CompanyID}" />

如果我将 ItemsSource 设置为静态属性,则可以工作。但是有时将 ItemsSource 设置为静态属性可能是可行的,但并不总是我想要的。


2
我仍然希望微软能够修复这个漏洞。 - juFo
使用xmlns:local作为命名空间是添加xmlns:vm的替代方案。 - Luke Vanzweden

49

正确的解决方案似乎是:

<Window.Resources>
    <CollectionViewSource x:Key="ItemsCVS" Source="{Binding MyItems}" />
</Window.Resources>
<!-- ... -->
<DataGrid ItemsSource="{Binding MyRecords}">
    <DataGridComboBoxColumn Header="Column With Predefined Values"
                            ItemsSource="{Binding Source={StaticResource ItemsCVS}}"
                            SelectedValueBinding="{Binding MyItemId}"
                            SelectedValuePath="Id"
                            DisplayMemberPath="StatusCode" />
</DataGrid>

对我来说,上面的布局完全正常,而且应该也适用于其他人。尽管这种设计选择没有得到很好的解释,但它也是有道理的。但如果您有一个包含预定义值的数据列,在运行时通常不会更改这些值。因此,创建一个 CollectionViewSource 并初始化数据一次是有意义的。它还可以消除查找祖先并绑定其数据上下文的较长绑定(这在我的感觉中总是不对的)。

我将这里留给任何其他遇到这个绑定困难的人,并想知道是否有更好的方法(因为显然这个页面仍然出现在搜索结果中,这就是我到这里来的方式)。


2
尽管这可能是一个不错的答案,但它也许与 OP 的问题有些抽象。如果在 OP 的代码中使用你的 MyItems,会导致编译错误。 - user585968
最近在做一个WPF项目,遇到了这个问题。我觉得这个答案是最简洁和最容易的。 - undefined

24

我知道这个问题已经超过一年了,但我刚在处理类似的问题时偶然发现了它,想分享另一个潜在的解决方案,以便帮助将来的旅行者(或者当我忘记这个问题后,在桌子上寻找最近的物体时,在StackOverflow上翻转)。

在我的情况下,我能够通过使用DataGridTemplateColumn而不是DataGridComboBoxColumn来获得所需的效果,如下面的代码片段所示。[注:我使用的是.NET 4.0,并且我所阅读的内容让我相信DataGrid已经发生了很多变化,因此如果使用较早版本,YMMV]

<DataGridTemplateColumn Header="Identifier_TEMPLATED">
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <ComboBox IsEditable="False" 
                Text="{Binding ComponentIdentifier,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                ItemsSource="{Binding Path=ApplicableIdentifiers, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding ComponentIdentifier}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

在苦苦挣扎了前几个答案之后,我尝试了这个方法,它也对我起作用了。谢谢。 - coson

8

RookieRick是对的,使用DataGridTemplateColumn而不是DataGridComboBoxColumn可以使XAML更简单。

此外,直接从GridItem访问CompanyItem列表可以省去RelativeSource

在我看来,这为您提供了一个非常清晰的解决方案。

XAML:

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding GridItems}" >
    <DataGrid.Resources>
        <DataTemplate x:Key="CompanyDisplayTemplate" DataType="vm:GridItem">
            <TextBlock Text="{Binding Company}" />
        </DataTemplate>
        <DataTemplate x:Key="CompanyEditingTemplate" DataType="vm:GridItem">
            <ComboBox SelectedItem="{Binding Company}" ItemsSource="{Binding CompanyList}" />
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Name}" />
        <DataGridTemplateColumn CellTemplate="{StaticResource CompanyDisplayTemplate}"
                                CellEditingTemplate="{StaticResource CompanyEditingTemplate}" />
    </DataGrid.Columns>
</DataGrid>

视图模型:

public class GridItem
{
    public string Name { get; set; }
    public CompanyItem Company { get; set; }
    public IEnumerable<CompanyItem> CompanyList { get; set; }
}

public class CompanyItem
{
    public int ID { get; set; }
    public string Name { get; set; }

    public override string ToString() { return Name; }
}

public class ViewModel
{
    readonly ObservableCollection<CompanyItem> companies;

    public ViewModel()
    {
        companies = new ObservableCollection<CompanyItem>{
            new CompanyItem { ID = 1, Name = "Company 1" },
            new CompanyItem { ID = 2, Name = "Company 2" }
        };

        GridItems = new ObservableCollection<GridItem> {
            new GridItem { Name = "Jim", Company = companies[0], CompanyList = companies}
        };
    }

    public ObservableCollection<GridItem> GridItems { get; set; }
}

4

您的ComboBox正在尝试绑定到GridItem[x].CompanyItems,但该项不存在。

您的相对绑定接近正确,但它需要绑定到DataContext.CompanyItems,因为Window.CompanyItems不存在。


谢谢回复!我已经尝试过了(在我问题中的最后一个XAML片段中,将 CompanyItems 替换为 DataContext.CompanyItems),但是调试器输出中依然出现同样的错误。 - Slauma
1
@Slauma 我不确定,但似乎应该可以工作。从你的XAML中我看到唯一不同寻常的地方是Mode=FindAncestor,而我通常会省略它。你是否尝试给你的根窗口一个名称,并在绑定时通过名称引用它,而不是使用RelativeSource?{Binding ElementName=RootWindow, Path=DataContext.CompanyItems} - Rachel
尝试过两种方法(省略Mode=FindAncestor并将绑定更改为命名元素),但它不起作用。奇怪的是这种方法对你起作用。我创建了这个简单的测试应用程序,将问题从我的应用程序中拖出来到一个非常简单的上下文中。我不知道我可能犯了什么错误,你在问题中看到的代码是完整的应用程序(从VS2010的WPF项目模板创建),这段代码周围没有其他内容。 - Slauma

2

这对我有效:


<DataGridTemplateColumn Width="*" Header="Block Names">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ComboBox
                VerticalContentAlignment="Center"
                ItemsSource="{Binding DataContext.LayerNames,
                RelativeSource={RelativeSource Findancestor,
                AncestorType={x:Type Window}}}"
                SelectedItem="{Binding LayerName, Mode=TwoWay,
                UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

这是唯一对我有效的方法..经过5个小时后。 - Tim Davis

2

我使用的最佳方法是将文本块和组合框绑定到同一属性,该属性应支持notifyPropertyChanged。

我使用相对资源将其绑定到父视图数据上下文(即UserControl),以在绑定中向上移动DataGrid级别。因为在这种情况下,DataGrid将搜索您在DataGrid.ItemsSource中使用的对象。

<DataGridTemplateColumn Header="your_columnName">
     <DataGridTemplateColumn.CellTemplate>
          <DataTemplate>
             <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.SelectedUnit.Name, Mode=TwoWay}" />
           </DataTemplate>
     </DataGridTemplateColumn.CellTemplate>
     <DataGridTemplateColumn.CellEditingTemplate>
           <DataTemplate>
            <ComboBox DisplayMemberPath="Name"
                      IsEditable="True"
                      ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.UnitLookupCollection}"
                       SelectedItem="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.SelectedUnit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                      SelectedValue="{Binding UnitId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                      SelectedValuePath="Id" />
            </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>

对我来说没问题,因为我使用的是用户控件而不是窗口。谢谢。 - surpavan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接