Привязка ItemsSource для ComboBoxColumn в WPF DataGrid

У меня есть два простых класса 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>

ViewModel установлен в главное окно DataContext в App.xaml.cs:

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();
    }
}

Как вы можете видеть, я установил ItemsSource DataGrid к GridItems Коллекция ViewModel. Эта часть работает, отображается одна строка сетки с именем "Джим".

Я также хочу установить ItemsSource ComboBox в каждом ряду до CompanyItems Коллекция ViewModel. Эта часть не работает: ComboBox остается пустым, и в окне вывода отладчика я вижу сообщение об ошибке:

Ошибка System.Windows.Data: 2: не удается найти управляющий FrameworkElement или FrameworkContentElement для целевого элемента. BindingExpression:Path=CompanyItems; DataItem= NULL; целевым элементом является DataGridComboBoxColumn (HashCode=28633162); Свойство target - "ItemsSource" (тип "IEnumerable")

Я полагаю, что WPF ожидает CompanyItems быть собственностью GridItem что не так, и это причина, по которой связывание не выполняется.

Я уже пытался работать с RelativeSource а также AncestorType вот так:

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

Но это дает мне еще одну ошибку в выводе отладчика:

Ошибка System.Windows.Data: 4: Не удается найти источник для привязки со ссылкой 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=CompanyItems; DataItem= NULL; целевым элементом является DataGridComboBoxColumn (HashCode=1150788); Свойство target - "ItemsSource" (тип "IEnumerable")

Вопрос: Как я могу связать ItemsSource DataGridComboBoxColumn с коллекцией CompanyItems ViewModel? Это вообще возможно?

Заранее спасибо за помощь!

8 ответов

Решение

Пожалуйста, проверьте, будет ли работать 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

Документация на MSDN о ItemsSource из DataGridComboBoxColumn говорит, что только статические ресурсы, статический код или встроенные коллекции элементов комбинированного списка могут быть связаны с ItemsSource:

Чтобы заполнить раскрывающийся список, сначала установите свойство ItemsSource для ComboBox, используя один из следующих параметров:

  • Статический ресурс. Для получения дополнительной информации см. Расширение разметки StaticResource.
  • X: Статический кодовый объект. Для получения дополнительной информации см. X: Расширение статической разметки.
  • Встроенная коллекция типов ComboBoxItem.

Привязка к свойству DataContext невозможна, если я правильно понимаю.

И действительно: когда я делаю CompanyItems статическое свойство во ViewModel...

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

... тогда это работает. Но иногда можно использовать ItemSource в качестве статического свойства, но это не всегда то, что я хочу.

Правильное решение кажется:

<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 и инициализация данных однажды имеет смысл. Он также избавляет от более длинных привязок, чтобы найти предка и привязать его к контексту данных (что всегда казалось мне неправильным).

Я оставляю это здесь для всех, кто боролся с этим связыванием и задавался вопросом, был ли лучший способ (поскольку эта страница, очевидно, все еще появляется в результатах поиска, вот как я сюда попал).

Я понимаю, что этому вопросу больше года, но я наткнулся на него, имея дело с подобной проблемой, и подумал, что поделюсь другим потенциальным решением в случае, если оно поможет будущему путешественнику (или мне самому, когда я забуду об этом позже и найду себя шлепается по Stackru между криками и бросками ближайшего объекта на моем столе).

В моем случае я смог получить желаемый эффект, используя DataGridTemplateColumn вместо DataGridComboBoxColumn, как в следующем фрагменте. [caveat: я использую.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>

RookieRick прав, используя DataGridTemplateColumn вместо DataGridComboBoxColumn дает гораздо более простой XAML.

Кроме того, положить CompanyItem список напрямую доступен из GridItem позволяет избавиться от 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; }
}

Ваш ComboBox пытается привязать к GridItem[x].CompanyItems, которого не существует

Ваше RelativeBinding близко, однако оно должно быть привязано к DataContext.CompanyItems потому что Window.CompanyItems не существует

Это работает для меня

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

В большинстве случаев я использую текстовый блок и комбинированный список с одним и тем же свойством, и это свойство должно поддерживать notifyPropertyChanged.

я использовал относительный ресурс для привязки к родительскому представлению datacontext, который является usercontrol для повышения уровня сетки данных в привязке, потому что в этом случае сетка данных будет искать в объекте, который вы использовали в datagrid.itemsource

<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>
Другие вопросы по тегам