DataSource for Entity Framework in WPF > Simplifying MVVM |
The Model-View-View-Model (MVVM) pattern is gaining in popularity as developers realize the benefits it gives their applications, making them easier to maintain and test and, particularly in the case of WPF and Silverlight applications, allowing a much clearer division of labor between the designer of the UI and the creator of the code that makes it work. However, without effective tools to aid program development based on MVVM patterns, programmers can actually find themselves working harder because of the need to implement an extra layer (the View Model) and ensure that data is properly synchronized between that and the Model. This extra burden isn’t onerous when you have a relatively small application with just a few simple collections (and best practice with MVVM is to use ObservableCollection as the datasource), but the bigger it becomes and the more collections it spawns, the worse it becomes. Entity Framework DataSource (EF DataSource) can ease the burden.
EF DataSource lets you use live views as your view model. Just create live views over your model collections and use them as your view model. Live views are synchronized automatically with their sources (model), so you don’t need any synchronization code - it’s all automatic. And live views are much easier to create than to write your own view model classes using ObservableCollection and filling those collections manually in code. You have all the power of LINQ at your disposal to reshape model data into live views. So, not only does synchronization code disappear, but the code creating view models is dramatically simplified.
To demonstrate how easy it is to follow the MVVM pattern using live views, let’s create a form combining all features from two previous examples: the Category-Products master-detail from Working with Data Sources in Code|document=WordDocuments\C1DataStudio-WPF.docx;topic=Working with Data Sources in Code and the reshaping/filtering/ordering of data from Live Views|document=WordDocuments\C1DataStudio-WPF.docx;topic=Live Views. It will be a Category-Products view, showing non-discontinued products whose unit price is at least 30, ordered by unit price, and displaying a customized set of product fields in a master-detail form where the user can select a category to show the products of that category. We'll follow the MVVM pattern. The form (view), called CategoryProductsView, only hosts GUI controls (a combo box and a grid) and does not have any code except what is used to set the data source to the view model.
All logic is in the view model class, separate from the GUI. More exactly, there are three classes: CategoryViewModel, ProductViewModel, and CategoryProductsViewModel. The first two are simple classes defining properties with no additional code:
Visual Basic |
Copy Code
|
---|---|
Public Class CategoryProductsViewModel Private _scope As EntityClientScope Private _categories As ICollectionView Public Property Categories As ICollectionView Get Return _categories End Get Private Set(value As ICollectionView) _categories = value End Set End Property Private _products As ICollectionView Public Property Products As ICollectionView Get Return _products End Get Private Set(value As ICollectionView) _products = value End Set End Property Public Sub New() _scope = Application.ClientCache.CreateScope() Categories = From c In _scope.GetItems(Of Category)() Select New CategoryViewModel With { .CategoryID = c.CategoryID, .CategoryName = c.CategoryName } Products = From p In _scope.GetItems(Of Product)().AsFilteredBound(Function(p) p.CategoryID.Value) .BindFilterKey(Categories, "CurrentItem.CategoryID").Include("Supplier") Select New ProductViewModel With { .ProductID = p.ProductID, .ProductName = p.ProductName, .CategoryID = p.CategoryID, .CategoryName = p.Category.CategoryName, .SupplierID = p.SupplierID, .SupplierName = p.Supplier.CompanyName, .UnitPrice = p.UnitPrice, .QuantityPerUnit = p.QuantityPerUnit, .UnitsInStock = p.UnitsInStock, .UnitsOnOrder = p.UnitsOnOrder } End Sub End Class |
C# |
Copy Code
|
---|---|
public class CategoryViewModel { public virtual int CategoryID { get; set; } public virtual string CategoryName { get; set; } } public class ProductViewModel { public virtual int ProductID { get; set; } public virtual string ProductName { get; set; } public virtual int? CategoryID { get; set; } public virtual string CategoryName { get; set; } public virtual int? SupplierID { get; set; } public virtual string SupplierName { get; set; } public virtual decimal? UnitPrice { get; set; } public virtual string QuantityPerUnit { get; set; } public virtual short? UnitsInStock { get; set; } public virtual short? UnitsOnOrder { get; set; } } public class CategoryProductsViewModel { private C1.Data.Entities.EntityClientScope _scope; public System.ComponentModel.ICollectionView Categories { get; private set; } public System.ComponentModel.ICollectionView Products { get; private set; } public CategoryProductsViewModel() { if (App.ClientCache == null) return; _scope = App.ClientCache.CreateScope(); Categories = from c in _scope.GetItems<Category>() select new CategoryViewModel() { CategoryID = c.CategoryID, CategoryName = c.CategoryName }; Products = from p in _scope.GetItems<Product>().AsFilteredBound(p => p.CategoryID) .BindFilterKey(Categories, "CurrentItem.CategoryID").Include("Supplier") select new ProductViewModel() { ProductID = p.ProductID, ProductName = p.ProductName, CategoryID = p.CategoryID, CategoryName = p.Category.CategoryName, SupplierID = p.SupplierID, SupplierName = p.Supplier.CompanyName, UnitPrice = p.UnitPrice, QuantityPerUnit = p.QuantityPerUnit, UnitsInStock = p.UnitsInStock, UnitsOnOrder = p.UnitsOnOrder }; } } |
Basically, it contains just two LiveLinq statements, nothing more.
Similar to what we saw in Working with Data Sources in Code, using AsFilteredBound() gives us server-side filtering. In Working with Data Sources in Code, we connected the filter key (which is the Product.CategoryID property) to CategoryID selected by the user using a combo box event. Here we can’t do this because we must keep our code independent of GUI. So we use a BindFilterKey method to bind the filter key to the Category.CategoryID property of the item currently selected in the Categories collection.
The Include("Supplier") operator is not strictly necessary; it is used here for performance optimization. Without it, Entity Framework will fetch Supplier objects lazily, one-by-one, every time the user accesses an element of the Products collection whose Supplier was not yet fetched. This can cause delays, and it’s generally much less efficient to fetch data in single rows than in batches, so we opted out of lazy loading here using Include("Supplier"), which tells Entity Framework to fetch supplier information in the same query with products.
Finally, we need to specify data binding for the GUI controls in the form (view) CategoryProductsView.xaml. Following the MVVM pattern, we do it in XAML (not in code), as shown below:
XAML |
Copy Code
|
---|---|
<Grid> <Grid.DataContext> <local:CategoryProductsViewModel /> </Grid.DataContext> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <ComboBox HorizontalAlignment="Left" Margin="5" Name="comboBox1" Width="221" ItemsSource="{Binding Categories}" DisplayMemberPath="CategoryName" /> <DataGrid Grid.Row="1" AutoGenerateColumns="True" Name="dataGrid1" ItemsSource="{Binding Products}"/> </Grid> |
Defining DataContext like this,
XAML |
Copy Code
|
---|---|
<Grid.DataContext> <local:CategoryProductsViewModel /> </Grid.DataContext> |
we made our form create a CategoryProductsViewModel object as its data source, and we bound the combo box and the grid to it using {Binding Categories} and {Binding Products}.