Lesson 8 - Add Advanced End-user Filtering Functionality to Applications

  • 10 minutes to read

Applications generated by the Scaffolding Wizard support client-side filtering out-of-the-box. This is achieved with the GridControl filtering capabilities. It is also posible to add basic server-side filtering capabilities.

In this lesson, you will learn how to implement the advanced filtering functionality that allows you to select one of the predefined filters and create complex filters at runtime and store them as a part of application settings.

Step 1 - Filtering Infrastructure Overview

In the How to: Create a Custom Filter tutorial, filters are represented by lambda expressions. This makes it easy to apply filters at runtime, but it is very difficult to serialize/deserialize them if it is necessary to allow an end-user to create custom application settings and store them between application launches.

In this lesson, you will use DevExpress criteria operators as they can be easily serialized/deserialized and converted to lambda expressions to perform filtered data queries. Although using Criteria Operators in view model code makes the View Model layer DevExpress-dependent, the use of Criteria Operators will be localized in several classes, so the View Model layer will mostly remain Criteria Operator-unaware.

The DevExpress.OutlookInpiredApp and DevExpress.HybridApp projects already contain all files of the filtering infrastructure (the Filtering folder).

File/class

Description

CustomFilterView.xaml

CustomFilterView.xaml.cs

Represent a view for the CustomFilterViewModel.

CustomFilterViewModel

Used in a dialog for creating/editing a custom filter.

FilterInfo

A serializable class that stores filter display names and their string representations. A collection of FilterInfo objects stores all filters for the specific module.

FilterItem

A view model that represents a filter in a UI.

FilterTreeView.xaml

FilterTreeView.xaml.cs

Represent a predefined and a custom filter tree in UI in the DevExpress.OutlookInspiredApp project.

FilterTreeViewModel

The main filtering view model that provides data for binding collections of predefined and custom filters in a UI and operations with them.

IFilterTreeModelPageSpecificSettings

An interface that encapsulates filter storage for the specific application module. The interface is implemented in the FilterTreeModelPageSpecificSettings class that stores and saves filters within application settings.

ISupportFiltering

An interface that allows you to apply a filter to a view model. This interface is implemented in a partial class for all collection view models. Since the CollectionViewModel class already contains the public FilterExpression property, the ISupportFiltering interface automatically gets an implicit implementation for it.

StaticFiltersPanel.xaml

StaticFiltersPanel.xaml.cs

Represent a predefined filter list in UI in the DevExpress.HybridApp project.

TreeViewSelectedItemBehavior

The class provides a simple custom implementation of the SelectedItem property which is bindable in two-way mode. It is necessary for showing filters using the standard TreeView control that does not provide such a property.

Step 2 - Storing Filters in Application Settings

Both sample projects already contain sets of predefined and custom filters within Properties/Settings.settings. To modify them, open the file in the designer, select a cell in the Value column and click the ellipsis button. The FilterInfo collection editor will appear. This editor allows you to add, remove FilterInfo objects and edit their properties:

outlook_tut_les8_1

Step 3 - Adding Filtering Features to the OutlookInspiredApp

Add the FiltersSettings class which can be found in the .../Filtering/ViewModel folder of the result application. A link to the result application is at the end of the lesson. This class provides factory methods to create an initialized FilterTreeViewModel for each module.

using DevExpress.DevAV.Common.ViewModel;
using DevExpress.DevAV.DevAVDbDataModel;
using DevExpress.Mvvm;
using DevExpress.Mvvm.POCO;
using DevExpress.OutlookInspiredApp.Properties;
using System;

namespace DevExpress.DevAV.ViewModels {
    internal static class FiltersSettings {
        public static FilterTreeViewModel<Employee, long> GetEmployeesFilterTree(object parentViewModel) {
            return FilterTreeViewModel<Employee, long>.Create(
                new FilterTreeModelPageSpecificSettings<Settings>(Settings.Default, "Status", x => x.EmployeesStaticFilters, x => x.EmployeesCustomFilters, null,
                    new[] {
                        BindableBase.GetPropertyName(() => new Employee().FullName),
                        BindableBase.GetPropertyName(() => new Employee().Id),
                    }),
                CreateUnitOfWork().Employees, (recipient, handler) => RegisterEntityChangedMessageHandler<Employee, long>(recipient, handler)
            ).SetParentViewModel(parentViewModel);
        }
        public static FilterTreeViewModel<Customer, long> GetCustomersFilterTree(object parentViewModel) {
            return FilterTreeViewModel<Customer, long>.Create(
                new FilterTreeModelPageSpecificSettings<Settings>(Settings.Default, "Favorites", x => x.CustomersStaticFilters, x => x.CustomersCustomFilters, null,
                    new[] {
                        BindableBase.GetPropertyName(() => new Customer().Id),
                    }),
                CreateUnitOfWork().Customers, (recipient, handler) => RegisterEntityChangedMessageHandler<Customer, long>(recipient, handler)
            ).SetParentViewModel(parentViewModel);
        }
        public static FilterTreeViewModel<Product, long> GetProductsFilterTree(object parentViewModel) {
            return FilterTreeViewModel<Product, long>.Create(
                new FilterTreeModelPageSpecificSettings<Settings>(Settings.Default, "Category", x => x.ProductsStaticFilters, x => x.ProductsCustomFilters,
                    new[] {
                        BindableBase.GetPropertyName(() => new Product().Id),
                        BindableBase.GetPropertyName(() => new Product().EngineerId),
                        BindableBase.GetPropertyName(() => new Product().SupportId),
                        BindableBase.GetPropertyName(() => new Product().Support),
                    }),
                CreateUnitOfWork().Products, (recipient, handler) => RegisterEntityChangedMessageHandler<Product, long>(recipient, handler)
            ).SetParentViewModel(parentViewModel);
        }
        public static FilterTreeViewModel<Order, long> GetSalesFilterTree(object parentViewModel) {
            return FilterTreeViewModel<Order, long>.Create(
                new FilterTreeModelPageSpecificSettings<Settings>(Settings.Default, "Category", x => x.OrdersStaticFilters, x => x.OrdersCustomFilters,
                    new[] {
                        BindableBase.GetPropertyName(() => new Order().Id),
                        BindableBase.GetPropertyName(() => new Order().CustomerId),
                        BindableBase.GetPropertyName(() => new Order().EmployeeId),
                        BindableBase.GetPropertyName(() => new Order().StoreId),
                    },
                    new[] {
                        BindableBase.GetPropertyName(() => new Order().Customer) + "." + BindableBase.GetPropertyName(() => new Customer().Name),
                    }),
                CreateUnitOfWork().Orders, (recipient, handler) => RegisterEntityChangedMessageHandler<Order, long>(recipient, handler)
            ).SetParentViewModel(parentViewModel);
        }
        public static FilterTreeViewModel<Quote, long> GetOpportunitiesFilterTree(object parentViewModel) {
            return FilterTreeViewModel<Quote, long>.Create(
                new FilterTreeModelPageSpecificSettings<Settings>(Settings.Default, "Category", x => x.QuotesStaticFilters, null, null),
                CreateUnitOfWork().Quotes, (recipient, handler) => RegisterEntityChangedMessageHandler<Quote, long>(recipient, handler)
            ).SetParentViewModel(parentViewModel);
        }

        static IDevAVDbUnitOfWork CreateUnitOfWork() {
            return UnitOfWorkSource.GetUnitOfWorkFactory().CreateUnitOfWork();
        }

        static void RegisterEntityChangedMessageHandler<TEntity, TPrimaryKey>(object recipient, Action handler) {
            Messenger.Default.Register<EntityMessage<TEntity, TPrimaryKey>>(recipient, message => handler());
        }

    }
}

Add the FilterTreeViewModel property to the DevAVDbModuleDescription.

public partial class DevAVDbModuleDescription : ModuleDescription<DevAVDbModuleDescription> {
        public DevAVDbModuleDescription(string title, string documentType, string group, IFilterTreeViewModel filterTreeViewModel,
Func<DevAVDbModuleDescription, object> peekCollectionViewModelFactory = null)
            : base(title, documentType, group, peekCollectionViewModelFactory) {
            FilterTreeViewModel = filterTreeViewModel;
        }
        public IFilterTreeViewModel FilterTreeViewModel { get; private set; }
    }

Pass the FilterTreeViewModel object as a parameter to each module.

protected override DevAVDbModuleDescription[] CreateModules() {
    return new DevAVDbModuleDescription[] {
            new DevAVDbModuleDescription("Employees", "EmployeeCollectionView", TablesGroup, FiltersSettings.GetEmployeesFilterTree(this), GetPeekCollectionViewModelFactory(x => x.Employees)),
            new DevAVDbModuleDescription("Customers", "CustomerCollectionView", TablesGroup, FiltersSettings.GetCustomersFilterTree(this), GetPeekCollectionViewModelFactory(x => x.Customers)),
            new DevAVDbModuleDescription("Products", "ProductCollectionView", TablesGroup, FiltersSettings.GetProductsFilterTree(this), GetPeekCollectionViewModelFactory(x => x.Products)),
            new DevAVDbModuleDescription("Sales", "OrderCollectionView", TablesGroup, FiltersSettings.GetSalesFilterTree(this)),
            new DevAVDbModuleDescription("Opportunities", "QuoteCollectionView", TablesGroup, FiltersSettings.GetOpportunitiesFilterTree(this)),
    };
}

Update the filter tree view model when the active module is changed.

protected override void OnActiveModuleChanged(DevAVDbModuleDescription oldModule) {
            base.OnActiveModuleChanged(oldModule);
            if(ActiveModule != null && ActiveModule.FilterTreeViewModel != null)
                ActiveModule.FilterTreeViewModel.SetViewModel(DocumentManagerService.ActiveDocument.Content);
        }

Bind the UI to the FilterTreeViewModel.

By default, Scaffolding Wizard generates a UI, displaying the collapsed NavigationPaneView in the NavBarControl within the main view (DevAVDbView.xaml) . We will use this navbar to display the filter tree.

Open the DevAVDbViewModel.cs file and add the following line to the DevAVDbViewModel constructor:

NavigationPaneVisibility = NavigationPaneVisibility.Normal;

outlook_tut_les8_2

Change NavBarGroup content to FilterTreeView in the NavBarControl.ItemTemplate:

<dxn:NavBarGroup.Content>
                        <view:FilterTreeView DataContext="{Binding FilterTreeViewModel}" />
                    </dxn:NavBarGroup.Content>

The filter editing form should be shown in a dialog window. Let's use the DialogService for this purpose. In the Document Outline window, select the root user control. Open its smart tag and select the MVVM Behaviors and Services tab. Click the Add Service link and choose DialogService from menu.

outlook_tut_les8_3

Select the added service and set its Name property to FilterDialogService. Customize the appearance of the dialog window by using the DialogStyle property:

<dx:DialogService.DialogStyle>
            <Style TargetType="dx:DXDialogWindow">
                <Setter Property="ShowIcon" Value="False" />
                <Setter Property="SizeToContent" Value="WidthAndHeight" />
                <Setter Property="MinWidth" Value="500" />
                <Setter Property="MinHeight" Value="370" />
            </Style>
        </dx:DialogService.DialogStyle>

Run the application:

outlook_tut_les8_4

The following commands are available from the context menu of the filter treeview:

  • Duplicate Filter for all predefined filters (Status group in the employees module)
  • New Custom Filter for Status and Custom Filters groups
  • Modify Custom Filter, Duplicate Custom Filter, Delete Custom Filter for all custom filters.

Modify the Custom Filter and New Custom Filter commands to invoke the custom filter editing dialog that contains FilterControl and allows you to build complex filter criteria:

outlook_tut_les8_5

Step 4 - Adding Filtering Features to the HybridApp

Add the FiltersSettings class which can be found in the .../Filtering/ViewModel folder of the result application. A link to the result application is at the end of the lesson. This class provides factory methods to create an initialized FilterTreeViewModel for each module.

Filtering implementation in the HybridApp is slightly different. In the OutlookInpiredApp, both the predefined and custom filters are displayed outside collection views in the main view (DevAVDbView).

In the HybridApp, predefined filters will be shown within some collection views to provide a smooth animated sliding transition between modules. So, you need to provide a way to assign the FilterTreeViewModel to the collection view. You also need a POCO Command that creates a new custom filter by sending the CreateCustomFilterMessage message that is handled by the main FilterTreeViewModel.

namespace DevExpress.DevAV.Common.ViewModel {
    partial class CollectionViewModel<TEntity, TProjection, TPrimaryKey, TUnitOfWork> : ISupportFiltering<TEntity>, IFilterTreeViewModelContainer<TEntity, TPrimaryKey>
        where TEntity : class
        where TProjection : class
        where TUnitOfWork : IUnitOfWork {
        public virtual FilterTreeViewModel<TEntity, TPrimaryKey> FilterTreeViewModel { get; set; }
        public void CreateCustomFilter() {
            Messenger.Default.Send(new CreateCustomFilterMessage<TEntity>());
        }
    }
}

Add the FilterTreeViewModel property to the DevAVDbModuleDescription.

public partial class DevAVDbModuleDescription : ModuleDescription<DevAVDbModuleDescription> {
        public DevAVDbModuleDescription(string title, string documentType, string group, IFilterTreeViewModel filterTreeViewModel = null)
        : base(title, documentType, group, null) {
            FilterTreeViewModel = filterTreeViewModel;
        }
        public IFilterTreeViewModel FilterTreeViewModel { get; private set; }
    }

Change modules initialization as follows.

protected override DevAVDbModuleDescription[] CreateModules() {
            var modules = new DevAVDbModuleDescription[] {
                new DevAVDbModuleDescription("Tasks", "EmployeeTaskCollectionView", MyWorldGroup, FiltersSettings.GetTasksFilterTree(this)),
                new DevAVDbModuleDescription("Employees", "EmployeeCollectionView", MyWorldGroup, FiltersSettings.GetEmployeesFilterTree(this)),
                new DevAVDbModuleDescription("Products", "ProductCollectionView", OperationsGroup, FiltersSettings.GetProductsFilterTree(this)),
                new DevAVDbModuleDescription("Customers", "CustomerCollectionView", OperationsGroup, FiltersSettings.GetCustomersFilterTree(this)),
                new DevAVDbModuleDescription("Sales", "OrderCollectionView", OperationsGroup, FiltersSettings.GetSalesFilterTree(this)),
                new DevAVDbModuleDescription("Opportunities", "QuoteCollectionView", OperationsGroup, FiltersSettings.GetOpportunitiesFilterTree(this))
            };
            foreach(var module in modules) {
                DevAVDbModuleDescription moduleRef = module;
                //_ Action that shows module if it is not visible at the moment a filter is selected
                module.FilterTreeViewModel.NavigateAction = () => Show(moduleRef);
            }
            return modules;
        }

Update the filter tree view model when the active module is changed.

protected override void OnActiveModuleChanged(DevAVDbModuleDescription oldModule) {
        base.OnActiveModuleChanged(oldModule);
        if(ActiveModule != null && ActiveModule.FilterTreeViewModel != null)
            ActiveModule.FilterTreeViewModel.SetViewModel(DocumentManagerService.ActiveDocument.Content);
    }

Place the StaticFiltersPanel to the left of the ListView or the GridControl in the following modules: EmployeeCollectionView.xaml, EmployeeTaskCollectionView.xaml, ProductCollectionView.xaml.

<view:StaticFiltersPanel DockPanel.Dock="Left" />

Run the application - the predefined filters list is shown on the left:

outlook_tut_les8_6

Custom filters will be available for 2 modules: ProductCollectionView and CustomerCollectionView.

To enable a custom filter chooser, add the following setters to the TileBar.ItemContainerStyle property:

<Setter Property="ShowFlyoutButton" Value="{Binding FilterTreeViewModel.CustomFilters.Count, Converter={dxmvvm:CriteriaOperatorConverter Expression=This&gt;0}}" />
                <Setter Property="FlyoutContent" Value="{Binding}" />
                <Setter Property="FlyoutContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <dxnav:TileBar ItemsSource="{Binding FilterTreeViewModel.CustomFilters}" SelectedItem="{Binding FilterTreeViewModel.SelectedItem}">
                                <dxnav:TileBar.ItemContainerStyle>
                                    <Style TargetType="dxnav:TileBarItem">
                                        <Setter Property="Height" Value="40" />
                                        <Setter Property="Content" Value="{Binding Name}" />
                                        <Setter Property="VerticalContentAlignment" Value="Top" />
                                        <Setter Property="Background" Value="White" />
                                        <Setter Property="Foreground" Value="Black" />
                                        <Setter Property="FontSize" Value="12" />
                                    </Style>
                                </dxnav:TileBar.ItemContainerStyle>
                                <dxnav:TileBar.GroupHeaderStyle>
                                    <Style TargetType="dxnavi:TileBarGroupHeader">
                                        <Setter Property="Foreground" Value="White" />
                                    </Style>
                                </dxnav:TileBar.GroupHeaderStyle>
                            </dxnav:TileBar>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>

Next, you will add the Create Custom Filter button to the ProductCollectionView module. Open the ProductCollectionView.xaml file and run the designer. Select the AppBar at the bottom of the view, invoke its smart tag and click the Add AppBarButton link.

outlook_tut_les8_7

Select the added button, open its smart tag and set the AppBarButton.Label property to Custom Filter, BarItem.AllowGlyphTheming to true, GlyphStretch to None, and HorizontalAlignment to Right. Bind the BarItem.Command property to CreateCustomFilterCommand.

outlook_tut_les8_8

To assign an image to the button, open DevExpress Image Gallery for the Glyph property and select the CustomFilter.png image that is already present in the project.

outlook_tut_les8_9

Add the same button to the CustomerCollectionView module.

Run the application to see how the new functionality works. Select a custom filter from the TileBar flyout.

outlook_tut_les8_10

The Custom Filter button invokes the Customer Filter Editor.

outlook_tut_les8_11

Applications that contain the result of this lesson are available here: DevExpress.OutlookInspiredApp and DevExpress.HybridApp.

See Also