WPF — как привязать ICollectionView к сетке данных с помощью MVVM

Я новичок в WPF и следую этой ссылке, чтобы использовать первый метод кода для создания примера. И пример работает. https://msdn.microsoft.com/en-us/data/jj574514.aspx

Теперь я пытаюсь изменить его, чтобы следовать MVVM.

Вот XAML MainWindow

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WPFwithEFSampleCodeFirst" mc:Ignorable="d" x:Class="WPFwithEFSampleCodeFirst.MainWindow"
    Title="MainWindow" Height="352.134" Width="517.53" Loaded="Window_Loaded">

<Grid  Margin="0,0,0,-3">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0*"/>
        <ColumnDefinition Width="77*"/>
        <ColumnDefinition Width="25*"/>
    </Grid.ColumnDefinitions>
    <Button Content="Save" Grid.Column="2" HorizontalAlignment="Left" Margin="41,167,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
    <DataGrid Grid.ColumnSpan="2"  ItemsSource="{Binding Categories}" AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="32,10,0,0" VerticalAlignment="Top" Height="124" Width="330" >
        <DataGrid.Columns>
            <DataGridTextColumn  Width="SizeToHeader" Header="Category Id" Binding="{Binding Path = CategoryId}"/>
            <DataGridTextColumn  Width="SizeToHeader" Header="Name" Binding="{Binding Path = Name}"/>
        </DataGrid.Columns>
    </DataGrid>

    <DataGrid Grid.ColumnSpan="2" AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="32,153,0,0" VerticalAlignment="Top" Height="146" Width="330">
        <DataGrid.Columns>
            <DataGridTextColumn  Binding="{Binding CategoryId}" Header="Category Id" Width="SizeToHeader"/>
            <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="SizeToHeader"/>
            <DataGridTextColumn  Binding="{Binding ProductId}" Header="Product Id" Width="SizeToHeader"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Here is the MainWindowViewModel

 class MainWindowViewModel
{
    private ICollectionView _categoryView;

    public ICollectionView Categories
    {
        get { return _categoryView; }
    }

    ProductContext context = new ProductContext();

    public MainWindowViewModel()
    {
        IList<Category> categories = GetCategories();
        _categoryView = CollectionViewSource.GetDefaultView(categories);

    }

    public IList<Category> GetCategories()
    {
        return context.Categories.ToList();
    }
}

Я не знаю, как привязать вторую сетку данных к ViewModel. Я хотел бы иметь ту же функцию отображения Master-Details, что и в исходном примере.

Итак, как привязать товары в категориях ко второй сетке данных? Как правильно реализовать это с помощью MVVM?


Больше информации:

 public class Category
{
    public Category()
    {
        this.Products = new ObservableCollection<Product>();
    }

    public int CategoryId { get; set; }
    public string Name { get; set; }

    public virtual ObservableCollection<Product> Products { get; private set; }
} 

    public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }

    public int CategoryId { get; set; }
    public virtual Category Category { get; set; }
} 

    public class ProductContext : DbContext
{
    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
} 

person Apple Juice    schedule 27.04.2016    source источник


Ответы (3)


Привяжите свойство SelectedItem мастера DataGrid к свойству в ViewModel.

<DataGrid SelectedItem="{Binding SelectedCategory}" Grid.ColumnSpan="2" ItemsSource="{Binding Categories}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn  Width="SizeToHeader" Header="Category Id" Binding="{Binding Path = CategoryId}"/>
        <DataGridTextColumn  Width="SizeToHeader" Header="Name" Binding="{Binding Path = Name}"/>
    </DataGrid.Columns>
</DataGrid>

MainWindowViewModel

private Category _selectedCategory;

public Category SelectedCategory
{
    get { return _selectedCategory; }
    set
    {
        _selectedCategory = value;
        OnPropertyChanged("SelectedCategory");
        OnPropertyChanged("SelectedCategoryProducts");
    }
}

(для этого требуется, чтобы ваша модель представления реализовывала INotifyPropertyChanged. Метод OnPropertyChanged вызывает обработчик событий PropertyChanged)

Добавьте еще одно свойство, которое возвращает свойство продуктов выбранной категории.

public ObservableCollection<Product> SelectedCategoryProducts
{
    get
    {
        if (_selectedCategory == null) return null;

        return _selectedCategory.Products;
    }
}

Привяжите данные DataGrid к свойству SelectedCategoryProducts в модели представления.

<DataGrid ItemsSource="{Binding SelectedCategoryProducts}" Grid.ColumnSpan="2" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn  Binding="{Binding CategoryId}" Header="Category Id" Width="SizeToHeader"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="SizeToHeader"/>
        <DataGridTextColumn  Binding="{Binding ProductId}" Header="Product Id" Width="SizeToHeader"/>
    </DataGrid.Columns>
</DataGrid>
person Glen Thomas    schedule 27.04.2016
comment
Я изменил две вещи в вашем коде, и это сработало. 1. Тип SelectedCategoryProducts должен быть ObservableCollection‹Product› вместо Category 2. Связывание подробностей datagird изменено на ItemsSource={Binding SelectedCategoryProducts} вместо DataContext={Binding SelectedCategoryProducts, ElementName=MasterGrid} - person Apple Juice; 28.04.2016
comment
В исходном примере используется метод перетаскивания источников данных для привязки двух сеток данных master-detail, что очень просто. Когда мы перемещаем его в MVVM, это кажется непростым способом и приходится привязываться к общедоступной собственности. - person Apple Juice; 28.04.2016

Самый простой способ - привязать DataContext подробного DataGrid к свойству SelectedItem основного DataGrid.

<DataGrid x:Name="MasterGrid" Grid.ColumnSpan="2" ItemsSource="{Binding Categories}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn  Width="SizeToHeader" Header="Category Id" Binding="{Binding Path = CategoryId}"/>
        <DataGridTextColumn  Width="SizeToHeader" Header="Name" Binding="{Binding Path = Name}"/>
    </DataGrid.Columns>
</DataGrid>

<DataGrid DataContext="{Binding SelectedItem.Products, ElementName=MasterGrid}" Grid.ColumnSpan="2" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn  Binding="{Binding CategoryId}" Header="Category Id" Width="SizeToHeader"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="SizeToHeader"/>
        <DataGridTextColumn  Binding="{Binding ProductId}" Header="Product Id" Width="SizeToHeader"/>
    </DataGrid.Columns>
</DataGrid>
person Glen Thomas    schedule 27.04.2016
comment
Привет, можете ли вы проверить приведенное выше обновление относительно определения класса? Я пытаюсь связать продукты со своей второй таблицей сведений. Связывание SelectedItem у меня не работает. - person Apple Juice; 28.04.2016
comment
@AppleJuice Работает ли изменение пути привязки на SelectedItem.Products? - person Glen Thomas; 28.04.2016
comment
Нет, SelectedItem.Products не работает. Во второй таблице данных ничего не отображается. - person Apple Juice; 28.04.2016
comment
Хорошо, я покажу альтернативный подход в отдельном ответе - person Glen Thomas; 28.04.2016

С ICollectionView было бы стыдно не использовать его функции...

Сначала установите IsSynchronizedToCurrentItem="true" в первой сетке данных (категории).

Затем во втором DataGrid привяжите DataSource к ItemsSource="{Binding Categories.CurrentItem.Products}", где категории — это ваша модель представления ICollectionView.

Эффект IsSynchToCurrentItem=true заключается в том, что вам не нужно хранить свойство в вашей модели представления, чтобы отслеживать ваш текущий элемент, потому что ICollectionView делает это за вас.

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

В дополнение к этой функции, ICollectionView позволяет вам сортировать, фильтровать и группировать, не касаясь вашей исходной коллекции, и, что более всего, позволяет вам программно изменить текущий элемент В МОДЕЛИ ВЫ ПРОСМОТРИТЕ БЕЗ ВОПРОСОВ С КОНТРОЛЕМ XAML (и, следовательно, выбранная строка в соответствующем элементе управления Visual/XAML) с помощью таких методов, как MoveCurrentTo(object target), MovecurrentToFirst() и т. д....

Ваша бизнес-модель C# в порядке, поэтому ваш XAML будет выглядеть так:

<DataGrid Grid.ColumnSpan="2" IsSynchronizedToCurrentItem="true"  ItemsSource="{Binding Categories}" AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="32,10,0,0" VerticalAlignment="Top" Height="124" Width="330" >
    <DataGrid.Columns>
        <DataGridTextColumn  Width="SizeToHeader" Header="Category Id" Binding="{Binding Path = CategoryId}"/>
        <DataGridTextColumn  Width="SizeToHeader" Header="Name" Binding="{Binding Path = Name}"/>
    </DataGrid.Columns>
</DataGrid>

<DataGrid Grid.ColumnSpan="2" ItemsSource="{Binding Categories.CurrentItem.Products}" AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="32,153,0,0" VerticalAlignment="Top" Height="146" Width="330">
    <DataGrid.Columns>
        <DataGridTextColumn  Binding="{Binding CategoryId}" Header="Category Id" Width="SizeToHeader"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="SizeToHeader"/>
        <DataGridTextColumn  Binding="{Binding ProductId}" Header="Product Id" Width="SizeToHeader"/>
    </DataGrid.Columns>
</DataGrid>
person Bruno    schedule 03.08.2016