1

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; }
} 
Owen Lee
  • 349
  • 1
  • 5
  • 19

3 Answers3

0

Simplest way is to bind the DataContext of the detail DataGrid to the SelectedItem property of the master 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>
Glen Thomas
  • 10,190
  • 5
  • 33
  • 65
0

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>
Owen Lee
  • 349
  • 1
  • 5
  • 19
Glen Thomas
  • 10,190
  • 5
  • 33
  • 65
  • I changed two things in your code and it worked. 1. The type of SelectedCategoryProducts should be ObservableCollection instead of Category 2. The details datagird binding is changed to ItemsSource="{Binding SelectedCategoryProducts}" instead of DataContext="{Binding SelectedCategoryProducts, ElementName=MasterGrid}" – Owen Lee Apr 28 '16 at 12:47
  • The original example is using drag and drop data sources method to bind the two master-details datagrids which is very easy. When we move it to MVVM, seems no easy way and have to bind to public property. – Owen Lee Apr 28 '16 at 12:57
0

With a ICollectionView it would be a shame not to use its functionalities...

First, set the IsSynchronizedToCurrentItem="true" on you first datagrid (categories).

Then in your second DataGrid, bind the DataSource with ItemsSource="{Binding Categories.CurrentItem.Products}" Where Categories is your view model ICollectionView.

The effect of IsSynchToCurrentItem=true is that you don't need to hold a property in your viewmodel to keep track of your current item because the ICollectionView does that for you.

Then each time the user select a row in the datagrid, the currentitem will change in the viewmodel (and there is an event on the ICollectionView to notify so) et each time you will set the current item in your view model, the correponding row will be selected.

In addition to this feature, the ICollectionView enables you to sort, filter and group without touching to your source collection and most of all, it enables you to programatically change the current item IN YOU VIEW MODEL WITHOUT MESSING ARROUND WITH THE XAML CONTROL (and thus the selected row in the corresponding visual/XAML itemcontrol) by using the method such as MoveCurrentTo(object target), MovecurrentToFirst(), and so on....

Your business C# model is fine, so your XAML would look like that :

<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>
Bruno
  • 1,944
  • 13
  • 22