UXGridView Part II: Data Access the MVVM-way with QueryDescriptor
As Jimmy wrote in his blog post, our upcoming Grid control for Silverlight and WPF, UXGridView, allows you to perform data operation with MVVM pattern elegantly using QueryDescriptor. In this post, I will explain some of fundamental concepts of the QueryDescriptor in more practical ways.
Understanding QueryDescriptor
First let’s take a look the QueryDescriptor class below.
As you can see, the QueryDescriptor has three properties that hold the information about the query. It also has a QueryChanged event that will be raised when any of the QueryDescriptor’s properties are changed. The following illustration shows some examples on how query information is stored in the QueryDescriptor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
QueryDescriptor queryDescriptor = new QueryDescriptor(); // filtering // get records that have // (UnitPrice >= 0 AND UnitPrice < 50) OR (UnitPrice == 0) CompositeFilterDescriptorCollection groupFilter1 = new CompositeFilterDescriptorCollection(); groupFilter1.LogicalOperator = FilterCompositionLogicalOperator.And; groupFilter1.Add( new FilterDescriptor() { PropertyName = "UnitPrice", Operator = FilterOperator.IsGreaterThanOrEqualTo, Value = 0 } ); groupFilter1.Add( new FilterDescriptor() { PropertyName = "UnitPrice", Operator = FilterOperator.IsLessThan, Value = 50 } ); CompositeFilterDescriptorCollection groupFilter2 = new CompositeFilterDescriptorCollection(); groupFilter2.LogicalOperator = FilterCompositionLogicalOperator.And; groupFilter2.Add( new FilterDescriptor() { PropertyName = "UnitsInStock", Operator = FilterOperator.IsEqualTo, Value = 0 } ); queryDescriptor.FilterDescriptors.LogicalOperator = FilterCompositionLogicalOperator.Or; queryDescriptor.FilterDescriptors.Add(groupFilter1); queryDescriptor.FilterDescriptors.Add(groupFilter2); // paging // get the record 6 - 10 queryDescriptor.PageDescriptor.PageSize = 5; queryDescriptor.PageDescriptor.PageIndex = 1; // sorting // sort by category ascending then by product id descending queryDescriptor.SortDescriptors.Add( new SortDescriptor() { PropertyName = "CategoryID", Direction = System.ComponentModel.ListSortDirection.Ascending } ); queryDescriptor.SortDescriptors.Add( new SortDescriptor() { PropertyName = "ProductID", Direction = System.ComponentModel.ListSortDirection.Descending } ); |
With our data controls, the QueryDescriptor will be updated automatically whenever users perform data operations through our data controls such as paging, filtering and sorting. So all you need to do here is simply wiring up the QueryDescriptor to the data controls and listen to its QueryChanged event.
In the QueryChanged function delegate, you will need to parse the information in QueryDescriptor to a data operation command for your specific data source. Fortunately, our data provider libraries come with some methods allowing you to easily parse the QueryDescriptor into WCF RIA or DevForce data service.
Next, I will show you how to bind the QueryDescriptor to UXGridView, listen to its QueryChanged and perform the data operation.
Binding QueryDescriptor to UXGridView using MVVM Pattern
First, let’s create the ViewModel for our example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
using System.Collections.Specialized; using Intersoft.Client.Data.ComponentModel; namespace UXGridView.Samples.ViewModels { public class ListProductsViewModel : ViewModelBase { public ListProductsViewModel() { this._queryDescriptor = new QueryDescriptor(); } private INotifyCollectionChanged _products; private QueryDescriptor _queryDescriptor; public INotifyCollectionChanged Products { get { return this._products; } set { if (this._products != value) { this._products = value; this.OnPropertyChanged("Products"); } } } public QueryDescriptor QueryDescriptor { get { return this._queryDescriptor; } set { if (this._queryDescriptor != value) { if (this._queryDescriptor != null) this._queryDescriptor.QueryChanged -= new System.EventHandler(OnQueryChanged); this._queryDescriptor = value; this._queryDescriptor.QueryChanged += new System.EventHandler(OnQueryChanged); this.OnPropertyChanged("QueryDescriptor"); } } } public virtual void LoadProducts() { } private void OnQueryChanged(object sender, System.EventArgs e) { this.LoadProducts(); } } } |
Notice that the LoadProducts() is still empty now, we’ll get to that later. Next we will bind this to our UXGridView in our View.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<Intersoft:UXPage 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" mc:Ignorable="d" xmlns:Intersoft="http://intersoft.clientui.com/schemas" xmlns:ViewModels="clr-namespace:UXGridView.Samples.ViewModels" x:Class="UXGridView.Samples.Views.UXGridView.ListProducts" Title="ListProducts Page" d:DesignWidth="640" d:DesignHeight="480"> <Grid x:Name="LayoutRoot"> <Grid.DataContext> <ViewModels:ListProductsViewModel/> </Grid.DataContext> <Grid MaxWidth="700" Margin="12"> <Intersoft:UXGridView Margin="8" AutoGenerateColumns="False" ItemsSource="{Binding Products}" QueryOperation="Server" SortDescriptors="{Binding QueryDescriptor.SortDescriptors}" PageDescriptor="{Binding QueryDescriptor.PageDescriptor}" PageSize="20" CanUserPage="True"> <Intersoft:UXGridView.Columns> <Intersoft:UXGridViewTextColumn Header="Category ID" Binding="{Binding CategoryID}"/> <Intersoft:UXGridViewTextColumn Header="Product ID" Binding="{Binding ProductID}" Aggregate="Count" FooterFormatString="Count = {0}"/> <Intersoft:UXGridViewTextColumn Header="Product Name" Binding="{Binding ProductName}"/> <Intersoft:UXGridViewTextColumn Header="Unit Price" Binding="{Binding UnitPrice}" Aggregate="Avg" FooterFormatString="Avg = {0}"/> <Intersoft:UXGridViewTextColumn Header="Units In Stock" Binding="{Binding UnitsInStock}" Aggregate="Max" FooterFormatString="Max = {0}"/> <Intersoft:UXGridViewTextColumn Header="Units On Order" Binding="{Binding UnitsOnOrder}" Aggregate="Min" FooterFormatString="Min = {0}"/> <Intersoft:UXGridViewTextColumn Header="Quantity Per Unit" Binding="{Binding QuantityPerUnit}"/> </Intersoft:UXGridView.Columns> </Intersoft:UXGridView> </Grid> </Grid> </Intersoft:UXPage> |
Note that you will need to set the QueryOperation to Server to enable server-side data operation. That’s required because UXGridView also has the capability to manipulate the data at client side, which is the default setting. For now, let’s focus on the server-side query mode.
The QueryChanged event of the QueryDescriptor will be raised when it is bind to any of our data controls such as UXGridView, UXDataFilter or UXDataPager. The event will also be raised whenever there are changes in the QueryDescriptor, so it is the only place where you want to handle all data operations.
Now, let’s start processing this QueryDescriptor and retrieve a piece of data from our repository.
Parsing QueryDescriptor and Retrieving Data from WCF RIA
To parse the QueryDescriptor to WCF RIA, you need to include the Intersoft.Client.Data.Provider.Ria assembly in your project. This data provider assembly provides several methods that will allow you to easily parse the QueryDescriptor to the WCF RIA data service.
Now, let’s parse our QueryDescriptor and write some code to load the data in the LoadProducts() method that we’ve prepared in the previous section.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
private NorthwindDomainContext _manager; private NorthwindDomainContext Manager { get { if (this._manager == null) this._manager = new NorthwindDomainContext(); return this._manager; } } public virtual void LoadProducts() { if (Intersoft.Client.Framework.ISControl.IsInDesignModeStatic) return; <strong> var query = this.Manager.GetProductsQuery() .OrderBy(p => p.ProductID) .Parse(this.QueryDescriptor); query.IncludeTotalCount = true; </strong> this.Manager.Load( query, op => { if (op.IsComplete) { this.Products = new PagedCollectionView(op.Entities); if (op.TotalEntityCount != -1) this.QueryDescriptor.PageDescriptor.TotalItemCount = op.TotalEntityCount; } else { // error handling } }, true); } private void OnQueryChanged(object sender, System.EventArgs e) { this.LoadProducts(); } |
As you can see, the implementation is very straightforward, you just need to call .Parse(this.QueryDescriptor) to produce a query that WCF RIA can process. Note that IncludeTotalCount is set to True, which is important in paging scenarios as we will need to set the QueryDescriptor.PageDescriptor.TotalItemCount property to the total entity count of the particular query. This enables the data pager UI to determine the total number of pages available.
Parsing QueryDescriptor and Retrieving Data from Dev Force
In addition to the WCF RIA, you can also parse the QueryDescriptor to a DevForce service using similar approaches, with minor adjustments. First, you need to include the Intersoft.Client.Data.Provider.DevForce assembly in your project. This data provider provides similar methods as available in the RIA counterpart. Next, you need to adjust some of the code such as declaring the relavant DevForce type, see below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
private NorthwindEntities _manager; private NorthwindEntities Manager { get { if (this._manager == null) this._manager = new NorthwindEntities(); return this._manager; } } public virtual void LoadProducts() { if (Intersoft.Client.Framework.ISControl.IsInDesignModeStatic) return; <strong>this.Manager.Products .OrderBy(p => p.ProductID).Parse(this.QueryDescriptor) .ExecuteAsync( op => { if (op.CompletedSuccessfully) { this.Products = new PagedCollectionView(op.Results); } else { // error handling } } ); </strong> } public virtual void GetTotalItemCount() { <strong>var queryCount = this.Manager.Products .Parse(this.QueryDescriptor, false).AsScalarAsync().Count(); queryCount.Completed += (o, e) => { if (e.Result != -1) this.QueryDescriptor.PageDescriptor.TotalItemCount = e.Result; }; </strong> } private void OnQueryChanged(object sender, System.EventArgs e) { this.GetTotalItemCount(); this.LoadProducts(); } |
Notice that you need to retrieve the total item count in two separate calls. This is required since DevForce handles the total item count retrieval differently.
I hope you agree that the QueryDescriptor makes MVVM and data service significantly easier and straightforward to implement. Also, remember that the QueryDescriptor can be shared across multiple controls such as UXDataFilter and UXDataPager, in addition to the UXGridView.
In my next post, I will cover about some nice Grid features that we shipped in the first CTP release. For now, download the CTP1 bits here, and enjoy building your data-centric application the MVVM way.
Best Regards,
Andry
Having used the grid for 3 days now, please visit my blog and read my initial thoughts on the grid. http://billgower.wordpress.com