Intersoft ClientUI 8 > ClientUI Fundamentals > Data Access Overview > QueryDescriptor Overview |
When building scalable data applications, developers should consider implementing the best practice in both the design and architecture to deliver applications that are responsive, maintainable and scalable. One of the common data access practice is to implement server data operation to increase the overall performance of the client application. Server data operation processes the query in the server and returns shaped results to the client. For more information about the basics of data access and the data operation types, see Data Access Overview.
Most of the built-in Silverlight data controls support only client data operation which require complete data source to work properly. On the other hands, ClientUI data controls are designed to fully support server data operation enabling you to leverage the best implementation for scalable and high-performance data applications.
Controls that support server data operation are built with advanced architecture in which it allows the data operation to be completely delegated to the server by providing the necessary query information to the developers. In this case, these controls are no longer required to process further actions in the client-side. The processes of the entire data life cycle – from capturing the query information from the views, forwarding the query to the service, to processing the final results – require a solid data component that manages and streamlines these processes in order to deliver reliable and consistent results.
ClientUI includes a built-in data component, named QueryDescriptor, to provide an elegant solution to address the requirements discussed above. This topic provides a comprehensive overview of the QueryDescriptor which contains the following sections:
QueryDescriptor is an intermediary object that contains the query information which you can use to process the query to a service for data retrieval. The query information is stored in three different properties as shown in the following figure.
As you can see in the above figure, there are three properties which contain specific query information.
QueryDescriptor provides a QueryChanged event that will be raised when any of the descriptor properties change. The QueryChanged event serves as a centralized pipeline where you want to handle all the query changes due to the actions in data controls.
CS |
Copy Code
|
---|---|
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"); } } } private void OnQueryChanged(object sender, System.EventArgs e) { // do data operation } |
The QueryDescriptor concept is specifically designed for MVVM pattern implementation which allows you to bind the QueryDescriptor to your ViewModel. You can listen to its QueryChanged event and handle the data operation based on the information stored in the QueryDescriptor. |
The following example shows how to instantiate a QueryDescriptor programmatically and demonstrate the use of FilterDescriptor, SortDescriptor and PageDescriptor in the QueryDescriptor object.
CS |
Copy Code
|
---|---|
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 } ); |
As discussed in the above section, server data operation processes the query entirely in the server side, while the data controls simply provide the query information. All ClientUI data controls centralized the query information in a QueryDescriptor object.
The following examples show how to bind the QueryDescriptor to data controls using MVVM pattern.
View Model |
Copy Code
|
---|---|
using System.Collections.Specialized; using Intersoft.Client.Data.ComponentModel; namespace UXGridView.Samples.ViewModels { public class ListProductsViewModel : ViewModelBase { public ListProductsViewModel() { this._queryDescriptor = new QueryDescriptor(); } private IEnumerable _products; private QueryDescriptor _queryDescriptor; public IEnumerable 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 not implemented in the above example and will be discussed in the latter section. Next, you bind the ViewModel to the UXGridView control.
Viwe |
Copy Code
|
---|---|
<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> |
Since the QueryDescriptor is bound to the ViewModel, you will now be able to handle data operation in the QueryChanged event handler. This is exactly the point where you can capture the query information from the QueryDescriptor and pass it to a data service for further processing.
ClientUI includes data providers that make it easy for you to parse the query information stored in the QueryDescriptor. It enables you to execute a server query and takes account the dynamic query provided in the QueryDescriptor in a single line of code. In this release, ClientUI provides two data providers, the first is designed for WCF RIA Services and the other is for DevForce Services. For more information about ClientUI data services, see Data Access Overview.
To parse the QueryDescriptor to EntityQuery in WCF RIA, you need to add the Intersoft.Client.Data.Provider.Ria assembly to your project references. This data provider assembly provides additional extensions methods to EntityQuery that will allow you to easily parse the QueryDescriptor to the EntityQuery in WCF RIA.
The following code shows how to parse the QueryDescriptor to an EntityQuery using WCF RIA Services.
View Model |
Copy Code
|
---|---|
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; var query = this.Manager.GetProductsQuery() .OrderBy(p => p.ProductID) .Parse(this.QueryDescriptor); query.IncludeTotalCount = true; 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 shown in the above example, the query implementation is straightforward. You just need to call the Parse() method to produce an EntityQuery that WCF RIA can process.
Note that the IncludeTotalCount is set to true, which is important in paging scenarios. When the query completes, the total entity count of the particular query is set to the QueryDescriptor.PageDescriptor.TotalItemCount property. This enables the data pager to determine the total number of available pages and synchronize the UI accordingly. |
To learn more how to implement data access with QueryDescriptor and WCF RIA Services using MVVM pattern, see Walkthrough: Bind UXGridView to WCF RIA Services using MVVM Pattern.
Similar to WCF RIA's EntityQuery, you can also parse the QueryDescriptor to a EntityQuery in DevForce using similar approaches, with minor adjustments. To get started, you need to add the Intersoft.Client.Data.Provider.DevForce assembly to your project references. This data provider assembly provides additional extensions methods to EntityQuery that will allow you to easily parse the QueryDescriptor to the EntityQuery in DevForce.
The following code shows how to parse the QueryDescriptor to an EntityQuery using DevForce Services.
View Model |
Copy Code
|
---|---|
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; this.Manager.Products .OrderBy(p => p.ProductID).Parse(this.QueryDescriptor) .ExecuteAsync( op => { if (op.CompletedSuccessfully) { this.Products = new PagedCollectionView(op.Results); } else { // error handling } } ); } public virtual void GetTotalItemCount() { 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; }; } private void OnQueryChanged(object sender, System.EventArgs e) { this.GetTotalItemCount(); this.LoadProducts(); } |
Note that you need to retrieve the total item count in the different server query. This is required because DevForce handles the total item count retrieval differently. |
To learn more how to implement data access the using QueryDescriptor and DevForce using MVVM pattern, see Walkthrough: Bind UXGridView to DevForce Services using MVVM Pattern.
Although QueryDescriptor is primarily built to support ClientUI data controls, you can eventually take advantage of its capability to store your own query information and parse it to your preferered data service for further data processing in your application.
Consider the following scenario where you have a search form to filter products data based on the input criterias.
In this case, you can populate your own QueryDescriptor based on the input fields and parse it to a data service such as shown in the following example.
View Model |
Copy Code
|
---|---|
using System.Collections; using System.ServiceModel.DomainServices.Client; using System.Windows; using HowToSamples.Web; using Intersoft.Client.Data.ComponentModel; using Intersoft.Client.Data.Provider.Ria; using Intersoft.Client.Framework.Input; namespace HowToSamples.ViewModels { public class FilterFormViewModel : ViewModelBase { public FilterFormViewModel() { this.QueryDescriptor = new QueryDescriptor(); this.Manager = new NorthwindDomainContext(); this.DoFilter = new DelegateCommand(ExecuteDoFilter); this.LoadProducts(); } private bool? _filterDiscontinued; private string _filterName; private IEnumerable _products; private QueryDescriptor _queryDescriptor; public DelegateCommand DoFilter { get; set; } private NorthwindDomainContext Manager { get; set; } public IEnumerable Products { get { return this._products; } set { if (this._products != value) { this._products = value; this.OnPropertyChanged("Products"); } } } public bool? FilterDiscontinued { get { return this._filterDiscontinued; } set { if (this._filterDiscontinued != value) { this._filterDiscontinued = value; this.OnPropertyChanged("FilterDiscontinued"); } } } public string FilterName { get { return this._filterName; } set { if (this._filterName != value) { this._filterName = value; this.OnPropertyChanged("FilterName"); } } } public QueryDescriptor QueryDescriptor { get { return this._queryDescriptor; } set { if (this._queryDescriptor != value) { this._queryDescriptor = value; this.OnPropertyChanged("QueryDescriptor"); } } } public void ExecuteDoFilter(object parameter) { this.QueryDescriptor.FilterDescriptors.Clear(); if (!string.IsNullOrEmpty(this.FilterName)) { FilterDescriptor filterName = new FilterDescriptor() { PropertyName = "ProductName", Operator = FilterOperator.StartsWith, Value = this.FilterName }; this.QueryDescriptor.FilterDescriptors.Add(filterName); } if (this.FilterDiscontinued.HasValue) { FilterDescriptor filterDiscontinued = new FilterDescriptor() { PropertyName = "Discontinued", Operator = FilterOperator.IsEqualTo, Value = this.FilterDiscontinued.Value }; this.QueryDescriptor.FilterDescriptors.Add(filterDiscontinued); } this.LoadProducts(); } public virtual void LoadProducts() { if (Intersoft.Client.Framework.ISControl.IsInDesignModeStatic) return; var query = this.Manager.GetProductsQuery().OrderBy(p => p.ProductID).Parse(this.QueryDescriptor); this.Manager.Load( query, op => { if (op.IsComplete) { this.Products = new PagedCollectionView(op.Entities); } else { MessageBox.Show(op.Error.ToString()); } }, true); } } } |
For more information about data filtering using QueryDescriptor, see How-to: Programmatically Filter Data using Query Descriptor.