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

82

У меня есть два простых класса модели и 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 установлен на MainWindow 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();
    }
}

Как видите, я установил ItemsSourceDataGrid в GridItemsколлекцию ViewModel. Эта часть работает, отображается единственная линия сетки с именем «Джим».

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

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

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

Я уже пробовал работать с a 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: Путь = CompanyItems; DataItem = null; целевой элемент - DataGridComboBoxColumn (HashCode = 1150788); целевым свойством является ItemsSource (тип IEnumerable)

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

Заранее благодарю за помощь!

Slauma
источник

Ответы:

123

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

serge_gubenko
источник
4
Черт, это работает !!! Если бы я только мог понять почему? И почему не использовать исходный код с изменениями, рекомендованными Рэйчел? В любом случае большое спасибо!
Slauma
1
Думаю, вы можете найти объяснение здесь: wpf.codeplex.com/workitem/8153?ProjectName=wpf (см. Комментарии)
serge_gubenko
1
Похоже, они решили превратить эту ошибку («Мы зарегистрировали ошибку в нашей внутренней базе данных, которая будет исправлена ​​в будущем выпуске») в функцию. Взгляните на мой собственный ответ в этой теме: проблема была решена с помощью документации, и это явный признак того, что это никогда не будет изменено.
Slauma
1
+1 за ссылку joemorrison.org/blog/2009/02/17/… . это решило мою проблему. Отстой, ~ 5 часов, и я понял, что у меня уже был этот тип в моем проекте для чего-то еще, что мы делали :( Это всегда процесс обучения.
TravisWhidden
У меня не работает. EditingElementStyle, похоже, работает, но по какой-то причине, когда я добавляю ElementStyle, я получаю ComboBoxes, которые ничего не заполняют (вместо значения из DisplayMemberPath), и он не переключается обратно на EditingElementStyle, когда я нажимаю.
Уильям
46

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

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

  • Статический ресурс. Дополнительные сведения см. В разделе Расширение разметки StaticResource.
  • Объект x: статический код. Дополнительные сведения см. В разделе x: Static Markup Extension.
  • Встроенная коллекция типов 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}" />

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

Slauma
источник
1
Я все еще надеюсь, что Microsoft исправит эту ошибку
juFo 02
38

Кажется, что правильное решение:

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

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

Адам Беккер
источник
1
Хотя, возможно, хороший ответ, он, возможно, абстрагируется от вопроса OP. При MyItemsиспользовании с кодом OP это приведет к ошибке компиляции
MickyD
22

Я понимаю, что этому вопросу больше года, но я просто наткнулся на него при решении аналогичной проблемы и подумал, что поделюсь другим потенциальным решением, если оно может помочь будущему путешественнику (или мне самому, когда я забуду об этом позже и обнаружу себя шлепаюсь на 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
7

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

Более того, размещение CompanyItemсписка, доступ к которому осуществляется напрямую из файла, GridItemпозволяет избавиться от файла RelativeSource.

IMHO, это дает вам очень чистое решение.

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, которой не существует.

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

Рэйчел
источник
Спасибо за ответ! Я пробовал это (заменено CompanyItemsна DataContext.CompanyItemsв последнем фрагменте XAML в моем вопросе), но это дает мне ту же ошибку в выводе отладчика.
Slauma
1
@Slauma Тогда я не уверен, это должно сработать. Единственное, что я вижу необычным в имеющемся у вас XAML, - это Mode = FindAncestor, и я обычно его опускаю. Вы пытались дать корневому окну имя и ссылаться на него по имени в привязке вместо использования RelativeSource? {Binding ElementName=RootWindow, Path=DataContext.CompanyItems}
Рэйчел
Пробовали обе вещи (пропустили Mode = FindAncestor и изменили привязку к именованному элементу), но это не работает. Странно, что у вас такой способ работает. Я создал это простое тестовое приложение, чтобы перетащить проблему из моего приложения в очень простой контекст. Я не знаю, что я мог сделать не так, код, который вы видите в вопросе, - это полное приложение (созданное из шаблона проекта WPF в VS2010), в этом коде больше ничего нет.
Slauma
1

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

Я использовал relativeresource для привязки к родительскому представлению datacontext, который является пользовательским контролем для повышения уровня datagrid в привязке, потому что в этом случае datagrid будет искать в объекте, который вы использовали в 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>
Хишам
источник