WPF DataGridTemplateColumn с привязкой к ComboBox (шаблон MVVM)

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

В этом сценарии DataContext для сетки ViewModel.Houses, а для ComboBox я хочу, чтобы ItemsSource привязывался к ViewModel.Owners.

Возможно ли это? Я собираюсь с этим справиться... лучшее, что я смог сделать, это правильно получить привязку ItemsSource, однако ComboBox (внутри DataGridTemplateColumn) не показывает правильные значения для House.HouseOwner в каждой строке.

ПРИМЕЧАНИЕ. Если я выберу ComboBox из изображения и поставлю TextBlock в DataTemplate, я могу правильно видеть значения для каждой строки, но получение как ItemsSource, так и показание правильного значения в выборе не является работая для меня...

Внутри моего кода я установил DataContext в окне ViewModel и в сетке, для DataContext установлено значение ViewModel.Houses. Для всего, кроме этого combobox, он работает...

Мой 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>

Порадовала бы какая-то помощь в этом... кажется, что требуется немного Voodoo, хотя...

Ответ 1

как указано в default.kramer, вам нужно удалить RelativeSource из ваших привязок для SelectedItem и SelectedValue следующим образом (обратите внимание, что вы должны добавить Mode=TwoWay к вашей привязке, чтобы изменение в combobox отражается в вашей модели).

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

SelectedValue/SelectedValuePath комбинация очень интересна. SelectedValuePath сообщает привязке данных, что свойство ID объекта, выбранного в данный момент Owner, представляет его значение, 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"}}
                };

(обратите внимание, что "HouseOwners" из коллекции "Дома" отличаются (новыми) от тех, что находятся в коллекции Owners). Однако следующее:

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

Надеюсь, что это поможет:)

Обновление: во втором случае вы можете получить тот же результат, не имея одинаковых ссылок, переопределив Equals в классе Owner (естественно, поскольку он использовался для сравнения объектов в первую очередь). (спасибо @ RJ Lohan, отметив это в комментариях ниже)

Ответ 2

Спасибо за помощь всем - я, наконец, выяснил, почему я не мог выбрать элементы ComboBox - был из-за обработчика события предварительного просмотра мыши, который я привязал к стилю ячейки, когда я использовал DataGridComboBoxColumn.

Похлопали себя за это, спасибо за другую помощь.

Кроме того, в качестве примечания; единственный способ, которым это будет работать для меня, - это дополнительное:

IsSynchronizedWithCurrentItem="False"

Добавлен в ComboBox, иначе они все покажут одинаковое значение по какой-то причине.

Кроме того, я не нуждаюсь в свойствах SelectedValue/SelectedValuePath в моем привязке, я считаю, потому что я переопределил Equals в моем связанном типе владельца.

И, наконец, я должен явно установить;

Режим = 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

Ответ 3

Это определенно возможно, и вы на правильном пути, используя привязку AncestorType для ItemsSource. Но я думаю, что вижу пару ошибок.

Во-первых, ваш ItemsSource должен быть привязкой к DataContext.Owners, а не DataContext.Houses, правильно? Вы хотите, чтобы в окне выпадающего списка отображалась коллекция Owners. Итак, сначала измените ItemsSource и вытащите материал, относящийся к выбору, например:

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

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

Что касается выбора, я думаю, вы должны быть привязаны только SelectedItem - not SelectedValue. Для этой привязки вы не хотите привязку RelativeSource - DataContext будет единственным House, поэтому вы можете напрямую связать его HouseOwner. Я предполагаю следующее:

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

Наконец, для отладки привязок вы можете увидеть окно вывода Visual Studio или подойти к инструменту, например Snoop или WPF Inspector. Если вы планируете делать много WPF, я бы рекомендовал начать работу со Snoop раньше, чем позже.

Ответ 4

Полный пример на основе предложения AbdouMoumen. Также удалены SelectedValue и SelectedValuePath.

введите описание изображения здесь

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