Pure Server-Side Filtering with RadGridView and WCF RIA Services
Those of you who are familiar with WCF RIA Services know that the DomainDataSource control provides a FilterDescriptors collection that enables you to filter data returned by the query on the server. We have been using this DomainDataSource feature in our RIA Services with DomainDataSource online example for almost an year now. In the example, we are listening for RadGridViews Filtering event in order to intercept any filtering that is performed on the client and translate it to something that the DomainDataSource will understand, in this case a System.Windows.Data.FilterDescriptor being added or removed from its FilterDescriptors collection. Think of RadGridView.FilterDescriptors as client-side filtering and of DomainDataSource.FilterDescriptors as server-side filtering. We no longer need the client-side one. With the introduction of the Custom Filtering Controls feature many new possibilities have opened. With these custom controls we no longer need to do any filtering on the client. I have prepared a very small project that demonstrates how to filter solely on the server by using a custom filtering control. As I have already mentioned filtering on the server is done through the FilterDescriptors collection of the DomainDataSource control. This collection holds instances of type System.Windows.Data.FilterDescriptor. The FilterDescriptor has three important properties: PropertyPath: Specifies the name of the property that we want to filter on (the left operand). Operator: Specifies the type of comparison to use when filtering. An instance of FilterOperator Enumeration. Value: The value to compare with (the right operand). An instance of the Parameter Class. By adding filters, you can specify that only entities which meet the condition in the filter are loaded from the domain context. In case you are not familiar with these concepts you might find Brad Abrams blog interesting. Now, our requirements are to create some kind of UI that will manipulate the DomainDataSource.FilterDescriptors collection. When it comes to collections, my first choice of course would be RadGridView. If you are not familiar with the Custom Filtering Controls concept I would strongly recommend getting acquainted with my step-by-step tutorial Custom Filtering with RadGridView for Silverlight and checking the online example out. I have created a simple custom filtering control that contains a RadGridView and several buttons. This control is aware of the DomainDataSource instance, since it is operating on its FilterDescriptors collection. In fact, the RadGridView that is inside it is bound to this collection. In order to display filters that are relevant for the current column only, I have applied a filter to the grid. This filter is a Telerik.Windows.Data.FilterDescriptor and is used to filter the little grid inside the custom control. It should not be confused with the DomainDataSource.FilterDescriptors collection that RadGridView is actually bound to. These are the RIA filters. Additionally, I have added several other features. For example, if you have specified a DataFormatString on your original column, the Value column inside the custom control will pick it up and format the filter values accordingly. Also, I have transferred the data type of the column that you are filtering to the Value column of the custom control. This will help the little RadGridView determine what kind of editor to show up when you begin edit, for example a date picker for DateTime columns. Finally, I have added four buttons two of them can be used to add or remove filters and the other two will communicate the changes you have made to the server. Here is the full source code of the DomainDataSourceFilteringControl. The XAML: <UserControl x:Class="PureServerSideFiltering.DomainDataSourceFilteringControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:telerikGrid="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.GridView" xmlns:telerik="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls" Width="300"> <Border x:Name="LayoutRoot" BorderThickness="1" BorderBrush="#FF8A929E" Padding="5" Background="#FFDFE2E5"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="150"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="2" Orientation="Horizontal" HorizontalAlignment="Center"> <telerik:RadButton Name="addFilterButton" Click="OnAddFilterButtonClick" Content="Add Filter" Margin="2" Width="96"/> <telerik:RadButton Name="removeFilterButton" Click="OnRemoveFilterButtonClick" Content="Remove Filter" Margin="2" Width="96"/> </StackPanel> <telerikGrid:RadGridView Name="filtersGrid" Grid.Row="1" Margin="2" ItemsSource="{Binding FilterDescriptors}" AddingNewDataItem="OnFilterGridAddingNewDataItem" ColumnWidth="*" ShowGroupPanel="False" AutoGenerateColumns="False" CanUserResizeColumns="False" CanUserReorderColumns="False" CanUserFreezeColumns="False" RowIndicatorVisibility="Collapsed" IsFilteringAllowed="False" CanUserSortColumns="False"> <telerikGrid:RadGridView.Columns> <telerikGrid:GridViewComboBoxColumn DataMemberBinding="{Binding Operator}" UniqueName="Operator"/> <telerikGrid:GridViewDataColumn Header="Value" DataMemberBinding="{Binding Value.Value}" UniqueName="Value"/> </telerikGrid:RadGridView.Columns> </telerikGrid:RadGridView> <StackPanel Grid.Row="2" Margin="2" Orientation="Horizontal" HorizontalAlignment="Center"> <telerik:RadButton Name="filterButton" Click="OnApplyFiltersButtonClick" Content="Apply Filters" Margin="2" Width="96"/> <telerik:RadButton Name="clearButton" Click="OnClearFiltersButtonClick" Content="Clear Filters" Margin="2" Width="96"/> </StackPanel> </Grid> </Border> </UserControl> And the code-behind: using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using Telerik.Windows.Controls.GridView; using System.Windows.Data; using Telerik.Windows.Controls; using Telerik.Windows.Data; namespace PureServerSideFiltering { /// <summary> /// A custom filtering control capable of filtering purely server-side. /// </summary> public partial class DomainDataSourceFilteringControl : UserControl, IFilteringControl { // The main player here. DomainDataSource domainDataSource; // This is the name of the property that this column displays. private string dataMemberName; // This is the type of the property that this column displays. private Type dataMemberType; /// <summary> /// Identifies the <see cref="IsActive"/> dependency property. /// </summary> /// <remarks> /// The state of the filtering funnel (i.e. full or empty) is bound to this property. /// </remarks> public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register( "IsActive", typeof(bool), typeof(DomainDataSourceFilteringControl), new PropertyMetadata(false)); /// <summary> /// Gets or sets a value indicating whether the filtering is active. /// </summary> /// <remarks> /// Set this to true if you want to lit-up the filtering funnel. /// </remarks> public bool IsActive { get { return (bool)GetValue(IsActiveProperty); } set { SetValue(IsActiveProperty, value); } } /// <summary> /// Gets or sets the domain data source. /// We need this in order to work on its FilterDescriptors collection. /// </summary> /// <value>The domain data source.</value> public DomainDataSource DomainDataSource { get { return this.domainDataSource; } set { this.domainDataSource = value; } } public System.Windows.Data.FilterDescriptorCollection FilterDescriptors { get { return this.DomainDataSource.FilterDescriptors; } } public DomainDataSourceFilteringControl() { InitializeComponent(); } public void Prepare(GridViewBoundColumnBase column) { this.LayoutRoot.DataContext = this; if (this.DomainDataSource == null) { // Sorry, but we need a DomainDataSource. Can't do anything without it. return; } // This is the name of the property that this column displays. this.dataMemberName = column.GetDataMemberName(); // This is the type of the property that this column displays. // We need this in order to see which FilterOperators to feed to the combo-box column. this.dataMemberType = column.DataType; // We will use our magic Type extension method to see which operators are applicable for // this data type. You can go to the extension method body and see what it does. ((GridViewComboBoxColumn)this.filtersGrid.Columns["Operator"]).ItemsSource = this.dataMemberType.ApplicableFilterOperators(); // This is very nice as well. We will tell the Value column its data type. In this way // RadGridView will pick up the best editor according to the data type. For example, // if the data type of the value is DateTime, you will be editing it with a DatePicker. // Nice! ((GridViewDataColumn)this.filtersGrid.Columns["Value"]).DataType = this.dataMemberType; // Yet another nice feature. We will transfer the original DataFormatString (if any) to // the Value column. In this way if you have specified a DataFormatString for the original // column, you will see all filter values formatted accordingly. ((GridViewDataColumn)this.filtersGrid.Columns["Value"]).DataFormatString = column.DataFormatString; // This is important. Since our little filtersGrid will be bound to the entire collection // of this.domainDataSource.FilterDescriptors, we need to set a Telerik filter on the // grid so that it will display FilterDescriptor which are relevane to this column ONLY! Telerik.Windows.Data.FilterDescriptor columnFilter = new Telerik.Windows.Data.FilterDescriptor("PropertyPath" , Telerik.Windows.Data.FilterOperator.IsEqualTo , this.dataMemberName); this.filtersGrid.FilterDescriptors.Add(columnFilter); // We want to listen for this in order to activate and de-activate the UI funnel. this.filtersGrid.Items.CollectionChanged += this.OnFilterGridItemsCollectionChanged; } /// <summary> // Since the DomainDataSource is a little bit picky about adding uninitialized FilterDescriptors // to its collection, we will prepare each new instance with some default values and then // the user can change them later. Go to the event handler to see how we do this. /// </summary> void OnFilterGridAddingNewDataItem(object sender, GridViewAddingNewEventArgs e) { // We need to initialize the new instance with some values and let the user go on from here. System.Windows.Data.FilterDescriptor newFilter = new System.Windows.Data.FilterDescriptor(); // This is a must. It should know what member it is filtering on. newFilter.PropertyPath = this.dataMemberName; // Initialize it with one of the allowed operators. // TypeExtensions.ApplicableFilterOperators method for more info. newFilter.Operator = this.dataMemberType.ApplicableFilterOperators().First(); if (this.dataMemberType == typeof(DateTime)) { newFilter.Value.Value = DateTime.Now; } else if (this.dataMemberType == typeof(string)) { newFilter.Value.Value = "<enter text>"; } else if (this.dataMemberType.IsValueType) { // We need something non-null for all value types. newFilter.Value.Value = Activator.CreateInstance(this.dataMemberType); } // Let the user edit the new filter any way he/she likes. e.NewObject = newFilter; } void OnFilterGridItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // We are active only if we have any filters define. In this case the filtering funnel will lit-up. this.IsActive = this.filtersGrid.Items.Count > 0; } private void OnApplyFiltersButtonClick(object sender, RoutedEventArgs e) { if (this.DomainDataSource.IsLoadingData) { return; } // Comment this if you want the popup to stay open after the button is clicked. this.ClosePopup(); // Since this.domainDataSource.AutoLoad is false, this will take into // account all filtering changes that the user has made since the last // Load() and pull the new data to the client. this.DomainDataSource.Load(); } private void OnClearFiltersButtonClick(object sender, RoutedEventArgs e) { if (this.DomainDataSource.IsLoadingData) { return; } // We want to remove ONLY those filters from the DomainDataSource // that this control is responsible for. this.DomainDataSource.FilterDescriptors .Where(fd => fd.PropertyPath == this.dataMemberName) // Only "our" filters. .ToList() .ForEach(fd => this.DomainDataSource.FilterDescriptors.Remove(fd)); // Bye-bye! // Comment this if you want the popup to stay open after the button is clicked. this.ClosePopup(); // After we did our housekeeping, get the new data to the client. this.DomainDataSource.Load(); } private void OnAddFilterButtonClick(object sender, RoutedEventArgs e) { if (this.DomainDataSource.IsLoadingData) { return; } // Let the user enter his/or her requirements for a new filter. this.filtersGrid.BeginInsert(); this.filtersGrid.UpdateLayout(); } private void OnRemoveFilterButtonClick(object sender, RoutedEventArgs e) { if (this.DomainDataSource.IsLoadingData) { return; } // Find the currently selected filter and destroy it. System.Windows.Data.FilterDescriptor filterToRemove = this.filtersGrid.SelectedItem as System.Windows.Data.FilterDescriptor; if (filterToRemove != null && this.DomainDataSource.FilterDescriptors.Contains(filterToRemove)) { this.DomainDataSource.FilterDescriptors.Remove(filterToRemove); } } private void ClosePopup() { System.Windows.Controls.Primitives.Popup popup = this.ParentOfType<System.Windows.Controls.Primitives.Popup>(); if (popup != null) { popup.IsOpen = false; } } } } Finally, we need to tell RadGridViews Columns to use this custom control instead of the default one. Here is how to do it: using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Windows.Data; using Telerik.Windows.Data; using Telerik.Windows.Controls; using Telerik.Windows.Controls.GridView; namespace PureServerSideFiltering { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); this.grid.AutoGeneratingColumn += this.OnGridAutoGeneratingColumn; // Uncomment this if you want the DomainDataSource to start pre-filtered. // You will notice how our custom filtering controls will correctly read this information, // populate their UI with the respective filters and lit-up the funnel to indicate that // filtering is active. Go ahead and try it. this.employeesDataSource.FilterDescriptors.Add(new System.Windows.Data.FilterDescriptor("Title", System.Windows.Data.FilterOperator.Contains, "Assistant")); this.employeesDataSource.FilterDescriptors.Add(new System.Windows.Data.FilterDescriptor("HireDate", System.Windows.Data.FilterOperator.IsGreaterThan, new DateTime(1998, 12, 31))); this.employeesDataSource.FilterDescriptors.Add(new System.Windows.Data.FilterDescriptor("HireDate", System.Windows.Data.FilterOperator.IsLessThanOrEqualTo, new DateTime(1999, 12, 31))); this.employeesDataSource.Load(); } /// <summary> /// First of all, we will need to replace the default filtering control /// of each column with out custom filtering control DomainDataSourceFilteringControl /// </summary> private void OnGridAutoGeneratingColumn(object sender, GridViewAutoGeneratingColumnEventArgs e) { GridViewBoundColumnBase dataColumn = e.Column as GridViewBoundColumnBase; if (dataColumn != null) { // We do not like ugly dates. if (dataColumn.DataType == typeof(DateTime)) { dataColumn.DataFormatString = "{0:d}"; // Short date pattern. // Notice how this format will be later transferred to the Value column // of the grid that we have inside the DomainDataSourceFilteringControl. } // Replace the default filtering control with our. dataColumn.FilteringControl = new DomainDataSourceFilteringControl() { // Let the control know about the DDS, after all it will work directly on it. DomainDataSource = this.employeesDataSource }; // Finally, lit-up the filtering funnel through the IsActive dependency property // in case there are some filters on the DDS that match our column member. string dataMemberName = dataColumn.GetDataMemberName(); dataColumn.FilteringControl.IsActive = this.employeesDataSource.FilterDescriptors .Where(fd => fd.PropertyPath == dataMemberName) .Count() > 0; } } } } The best part is that we are not only writing filters for the DomainDataSource we can read and load them. If the DomainDataSource has some pre-existing filters (like I have created in the code above), our control will read them and will populate its UI accordingly. Even the filtering funnel will light-up! Remember, the funnel is controlled by the IsActive property of our control. While this is just a basic implementation, the source code is absolutely yours and you can take it from here and extend it to match your specific business requirements. Below the main grid there is another debug grid. With its help you can monitor what filter descriptors are added and removed to the domain data source. Download Source Code. (You will have to have the AdventureWorks sample database installed on the default SQLExpress instance in order to run it.) Enjoy!Did you know that DotNetSlackers also publishes .net articles written by top known .net Authors? We already have over 80 articles in several categories including Silverlight. Take a look: here.