WinForms Tips & Tricks - Mastering Filter Editors

WinForms Team Blog
05 June 2019

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.

Filter Controls

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.

Standard Functions

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 code

  • DisplayName - 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 is Object, 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 Control

  • Category - 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 a bool result type.

  • Evaluate - the method called every time the function evaluates a data field for inclusion. Values are passed in the operands array. Return true 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);
  }
}

Registering For One Specific Control

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();
    }
  }
}

Registration Specific To Individual Properties

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;

Visual Criteria Display Style

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;
    }
  }
}

PopupMenuShowing

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 OnBeforeShowValueEditor(object sender, ShowValueEditorEventArgs e) {
  var node = e.CurrentNode as ClauseNodeEx;
  if(node != null && node.Property.Type == typeof(DateTime) &&
    Equals(node.FunctionType, WithinDaysOfTodayFunction.FunctionName))
    e.CustomRepositoryItem = spinEdit;
}

BeforeShowValueEditor

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:

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.