The DevExpress WinForms installation ships with two controls that allow end-users to build filter queries: a Filter Control that provides a GUI, and a Filter Editor Control that combines the Filter Control with a text input-based panel. Most data-aware controls in the WinForms line-up use these components, but you can also include them in your own forms and bind them to data-aware controls as required.
To illustrate, below is a Data Grid with its Filter Editor Control. Users can click the Edit Filter button in the filter panel to bring up the Filter Editor, and the text panel of the Filter Editor Control is visible because the property DefaultFilterEditorView is set to TextAndVisual
.
In the following image you can see some of the standard functions available in both controls, including Less or Equal, More or Equal, Is Today, Is Yesterday, and many others. Both Filter Control and Filter Editor Control provide a large variety of functions to choose from. The set of available functions varies depending on the type of the data field for which you’re building an expression.
Custom Functions
In some scenarios, the standard set of functions is not enough. We processed a large number of Support Center tickets in order to find the most frequently requested custom functions. Here are the three most popular scenarios:
- Inversions of standard functions, e.g. Does not begin with as the opposite of Begins with. Of course these inversions can be applied manually in the filter editor, but for frequent use a custom function is more convenient.
- Functions that represent combinations of standard functions for complex expressions. For example, a function Within Days of X could include rows where the field value is within N days before or after a given date. This saves users the effort of configuring the standard function In Between with two separate dates.
- Custom DateTime functions, like Is Weekend, N Days Ago and others
Starting with v19.1, both Filter Editor Control and Filter Control fully support custom functions, making it easy to implement the scenarios above and many others.
Technical Basics
A custom function is a class that implements the interface ICustomFunctionDisplayAttributes
. Note that you can additionally implement the interface ICustomFunctionOperatorFormattable
if you need server-side processing of your custom functions, but in the scope of this article we focus on ICustomFunctionDisplayAttributes
alone.
These are the methods and properties required for the interface implementation:
Name
- the technical function name that you use to refer to a custom function from codeDisplayName
- a readable function name displayed in the GUI. For instance, a function with the name NotStartsWith may have the DisplayName Does not start with.Image
- an icon shown for the function in Filter Control menus. The property type isObject
, but the samples below show how to use existing standard images. Image objects can also be assigned.Description
- a function description, displayed in a pop-up hint when users write expressions in the text panel of the Filter Editor ControlCategory
- the function category for the Expression Editor. This is not relevant if you plan to use your function only for filters.MinOperatorCount
,MaxOperatorCount
,IsValidOperandCount
- the number of operators supported by the function. For Filter and Filter Editor Controls, all three values can be equal. If you plan to use your functions in the Expression Editor, you can flexibly support variable numbers of operators.IsValidOperandType
- called for each operand to check whether it has a valid type. Only the first operand is checked for filters.ResultType
- the return value type of the function. The Filter Control shows only functions with abool
result type.Evaluate
- the method called every time the function evaluates a data field for inclusion. Values are passed in theoperands
array. Returntrue
to include the related row,false
to exclude it.
Finally, we recommend adding two static convenience functions Register
and Unregister
. This is an optional step, but the implementations are simple (see below) and they call into existing helpers on the CriteriaOperator
type.
Examples
For your reference, here are three examples that cover the three most requested scenarios mentioned above. This first custom function is called NotBeginsWith
, a negation of the standard function BeginsWith
.
public class NotBeginsWithFunction : ICustomFunctionDisplayAttributes { public const string FunctionName = "NotBeginsWith"; static readonly NotBeginsWithFunction instance = new NotBeginsWithFunction(); public static void Register() { CriteriaOperator.RegisterCustomFunction(instance); } public static bool Unregister() { return CriteriaOperator.UnregisterCustomFunction(instance); } public string Name => FunctionName; public string DisplayName => "Does not begin with"; public object Image => "FontSizeDecrease;Office2013"; public string Description => "Hides records when the field begins with the given value"; public FunctionCategory Category => FunctionCategory.Text; public int MinOperandCount => 2; public int MaxOperandCount => 2; public bool IsValidOperandCount(int count) => count == 2; public bool IsValidOperandType(int operandIndex, int operandCount, Type type) => type == typeof(string); public Type ResultType(params Type[] operands) => typeof(bool); public object Evaluate(params object[] operands) { if(operands[0] != null && operands[1] != null) { string str1 = operands[0].ToString(); string str2 = operands[1].ToString(); return !str1.StartsWith(str2, StringComparison.InvariantCultureIgnoreCase); } return false; } }
Here’s a second custom function WithinDaysOfToday
which checks whether a DateTime
value is within the timeframe Today - N days and Today + N days.
public class WithinDaysOfTodayFunction : ICustomFunctionDisplayAttributes { public const string FunctionName = "WithinDaysOfToday"; static readonly WithinDaysOfTodayFunction instance = new WithinDaysOfTodayFunction(); public static void Register() { CriteriaOperator.RegisterCustomFunction(instance); } public static bool Unregister() { return CriteriaOperator.UnregisterCustomFunction(instance); } public string Name => FunctionName; public string DisplayName => "Within days of today"; public object Image => "SwitchTimeScalesTo;Size16x16;Colored"; public string Description =>"Shows records when the field value within X days of today"; public FunctionCategory Category => FunctionCategory.DateTime; public int MinOperandCount => 2; public int MaxOperandCount => 2; public bool IsValidOperandCount(int count) => count == 2; public bool IsValidOperandType(int operandIndex, int operandCount, Type type) => operandIndex == 0 && type == typeof(DateTime) || operandIndex == 1 && type == typeof(int); public Type ResultType(params Type[] operands) => return typeof(bool); public object Evaluate(params object[] operands) { DateTime dt = Convert.ToDateTime(operands[0]); int days = Convert.ToInt32(operands[1]); DateTime start = DateTime.Today.AddDays(-days); DateTime end = DateTime.Today.AddDays(days); return dt >= start && dt <= end; } }
Finally, IsWeekend
tests whether a DateTime
value is Saturday or Sunday.
public class IsWeekendFunction : ICustomFunctionDisplayAttributes { public const string FunctionName = "IsWeekend"; static readonly IsWeekendFunction instance = new IsWeekendFunction(); public static void Register() { CriteriaOperator.RegisterCustomFunction(instance); } public static bool Unregister() { return CriteriaOperator.UnregisterCustomFunction(instance); } public string Name => FunctionName; public string DisplayName => "Is weekend"; public object Image => "DayView;Office2013"; public string Description => "Shows records when the field value is on Saturday or Sunday"; public FunctionCategory Category => FunctionCategory.DateTime; public int MinOperandCount => 1; public int MaxOperandCount => 1; public bool IsValidOperandCount(int count) => count == 1; public bool IsValidOperandType(int operandIndex, int operandCount, Type type) => type == typeof(DateTime); public Type ResultType(params Type[] operands) => typeof(bool); public object Evaluate(params object[] operands) { DateTime dt = Convert.ToDateTime(operands[0]); return dt.DayOfWeek == DayOfWeek.Sunday || dt.DayOfWeek == DayOfWeek.Saturday; } }
Registering Functions
When your custom functions are ready, you need to register them, i.e. add them to the supported function list for Filter Control and Filter Editor Control. If you included the optional Register and Unregister methods in your custom function classes, the registration code is quite short:
//Program.cs file namespace DXSample { static class Program { [STAThread] static void Main() { IsWeekendFunction.Register(); WithinDaysOfTodayFunction.Register(); NotBeginsWithFunction.Register(); // ... Application.Run(new Main()); } } }
Technically, your functions are now available. If you manually write an expression in the text panel of the Filter Editor Control and you use any of these custom functions, a valid filter criteria is generated. However, up to this point the functions will not be included in the visual panel.
Depending on your requirements, use one of the following three techniques to add your custom functions to the GUI.
One Specific Control
To make a function available only for one specific data-aware control and its embedded Filter Editor Control, implement a handler for the event QueryCustomFunctions
of the control. Using the code below, the IsWeekendFunction
is made available for a Data Grid in both embedded Filter Editor and Excel-style filter menus, while the function WithinDaysOfToday
is visible in the Filter Editor only.
gridView1.QueryCustomFunctions += OnQueryCustomFunctions; private void OnQueryCustomFunctions(object sender, DevExpress.XtraGrid.Views.Grid.CustomFunctionEventArgs e) { if(e.PropertyType == typeof(DateTime)) { e.Add(IsWeekendFunction.FunctionName); if(e.IsFilterEditor) e.Add(WithinDaysOfTodayFunction.FunctionName); } }
All Filter And Filter Editor Controls
To register global custom functions for inclusion in all Filter and Filter Editor controls, add them in a handler of the event CriteriaOperator.QueryCustomFunctions
. The function NotBeginsWith
is registered globally in this sample:
static class Program { [STAThread] static void Main() { // ... CriteriaOperator.QueryCustomFunctions += OnQueryCustomUIFunctions; // ... } private static void OnQueryCustomUIFunctions(object sender, DevExpress.Data.Filtering.CustomFunctionEventArgs e) { if(e.PropertyType == typeof(string)) { e.Add(NotBeginsWithFunction.FunctionName); } } }
Specific To Individual Properties
To register functions that should be available for all Filter and Filter Editor Controls but specific to properties on data types, annotate properties with the attribute DevExpress.Data.Filtering.CustomFunction
. In this sample, a Data Grid shows a type with two string
properties Text
and Info
. The custom function NotBeginsWith
is available only for the Info
field.
[CustomFunction(NotBeginsWithFunction.FunctionName /*, Image = <image>*/)] public string Info { get { return info; } set { if (info != value) { info = value; OnPropertyChanged(); } } }
Demo
If you have our demos installed on your machine, you can see these samples in the Advanced Filter Control Data Grid demo module.
Customization
In v19.1 we introduced the Visual
option for the FilterCriteriaDisplayStyle
setting. It combines visual changes to the Filter Panel and the Filter Control, which use little blocks painted with skin colors to show criteria. It also uses a drop-down menu instead of a combo box to select criteria when building expressions. We expect to improve this menu in the future, since it currently lacks a few features (incremental search and fixed height support), but it does arrange functions into groups as a definite improvement over the “non-visual” style where the list of functions can grow inconveniently long.
gridView1.OptionsView.FilterCriteriaDisplayStyle = DevExpress.XtraEditors.FilterCriteriaDisplayStyle.Visual;
The menu is fully customizable. The main entry point for such customization work is the event FilterEditorCreated
. It provides access to an instance of the FilterEditorControl
, which in turn has three events you can handle.
PopupMenuShowing
allows you to hide, remove and rename items, and change their icons for any Filter Editor Control menu. The relevant menu for this purpose has e.MenuType == FilterControlMenuType.Clause
:
gridView1.FilterEditorCreated += OnFilterEditorCreated; private void OnFilterEditorCreated(object sender, DevExpress.XtraGrid.Views.Base.FilterControlEventArgs e) { e.FilterEditor.PopupMenuShowing += OnPopupMenuShowing; } private void OnPopupMenuShowing(object sender, DevExpress.XtraEditors.Filtering.PopupMenuShowingEventArgs e) { if (e.MenuType == FilterControlMenuType.Clause) { var node = e.CurrentNode as ClauseNode; // customize function menus for DateTime fields if (node != null && node.Property.Type == typeof(DateTime)) { e.Menu.Hide(ClauseType.Equals); e.Menu.Remove(ClauseType.DoesNotEqual); e.Menu.Hide( DevExpress.XtraEditors.Controls.StringId.FilterAdvancedDateTimeOperatorMenuCaption); var menuItem = e.Menu.Find(ClauseType.Between); menuItem.Caption = "Between A and B"; menuItem.ImageOptions.SvgImage = MySvgImage1; } } }
Handle the event InitNode
to initialize new FilterEditor nodes created by users, for instance to assign default functions and operators. In the following example, if a user creates a new condition for the field ShippingDate
, the function IsWeekend
will be selected initially. The function Contains
is also configured as the default for all string
fields.
gridView1.FilterEditorCreated += OnFilterEditorCreated; private void OnFilterEditorCreated(object sender, DevExpress.XtraGrid.Views.Base.FilterControlEventArgs e) { e.FilterEditor.InitNode += OnInitNode; } private void OnInitNode(object sender, InitNodeEventArgs e) { if (e.IsNewNode) { e.PropertyName = "ShippingDate"; e.SetOperation("IsWeekend"); } else if (e.PropertyType == typeof(string)) e.SetOperation(FunctionOperatorType.Contains); }
Implement a handler for BeforeShowValueEditor
to customize value editors used in Filter Editor Controls. For example, most DateTime
functions work with date operands and use the Calendar editor. However, the custom function WithinDaysOfToday
(above) accepts an integer value for the number of days, and the code below changes the default Calendar editor to the SpinEdit control.
gridView1.FilterEditorCreated += OnFilterEditorCreated; private void OnFilterEditorCreated(object sender, DevExpress.XtraGrid.Views.Base.FilterControlEventArgs e) { e.FilterEditor.BeforeShowValueEditor += OnBeforeShowValueEditor; } private void OnQueryCustomFunctions(object sender, DevExpress.XtraGrid.Views.Grid.CustomFunctionEventArgs e) { if (e.PropertyType == typeof(DateTime)) { e.Add(IsWeekendFunction.FunctionName); if (e.IsFilterEditor) e.Add(WithinDaysOfTodayFunction.FunctionName); } }
Please Let Us Have Your Feedback
As always, we’re looking forward to your thoughts. We’re continuously working to improve our products, so please let us know if your use case is not covered by our implementation.
More Tips & Tricks Posts
This post is part of our Tips & Tricks blog series. These are other posts in the series: