Search code examples
c#wpfmvvmdatagridicollectionview

WPF - How to bind ICollectionView to datagrids using MVVM


I am new to WPF and following this link to use code first method to build the example. And the example works. https://msdn.microsoft.com/en-us/data/jj574514.aspx

Now, I am trying to change it to follow MVVM.

Here is the MainWindow XAML

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

I don't know how to binding the second details datagrid to ViewModel. I would like to have the same Master-Details display function as the original example.

So how to bind Products in Categories to the second datagrid? What is the right way to implement it using MVVM?


More info:

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

Solution

  • Bind the master DataGrid's SelectedItem property to a property in the 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");
        }
    }
    

    (this requires your view model to implement INotifyPropertyChanged. The OnPropertyChanged method invokes the PropertyChanged event handler)

    Add another property that returns the selected category's products property

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

    Bind the details DataGrid to the SelectedCategoryProducts property in the view model

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