XamGrid - Implementing Custom Column Filtering UI

Nikolay Zhekov / Wednesday, June 01, 2011

XamGrid has tons of features to make it easier for you to show and manipulate your data. It provides two different UI patterns for filtering - FilterRow and Excel-like Filter Menu. But what if these two do not fit your needs?

In this post I'll show you how I created a custom Filtering UI.

You can find the code at the end of the post.

Since the whole implementation is hundreds of line, I won't be able to guide you through all steps and to explain every line. I'll focus on the parts where the filtering happens and the usage of the IG Commanding Framework.

Adding HeaderDropDown

The first step was to create a class ColumnSearchBoxControl that derives from Control. This control will be displayed in the popup of a HeaderDropDown control. The only template part will be the TextBox.

Then I opened the genric.xaml file of the XamGrid and copied the style of the HeaderCellControl and all brushes and styles that are used in it.

With that done I was ready to create the ControlTemplate of the ColumnSearchBoxControl and place it in the HeaderCellControl.

<Grid Grid.Column="2"
      x:Name="SortAndPinIndicators">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    ...
    <!-- Default XamGrid Indicators and menus -->
    ...

    <igPrim:HeaderDropDownControl Grid.Column="5">
        <igPrim:HeaderDropDownControl.OpenButtonContent>
            <Grid>
                ...
                <!-- Icon Displayed in the HeaderCellControl -->
                ...
            </Grid>
        </igPrim:HeaderDropDownControl.OpenButtonContent>

        <!-- The ColumnSearchBoxControl displayed in popup when the Icon in the header is clicked -->
        <local:ColumnSearchBoxControl ... >
        </local:ColumnSearchBoxControl>
    </igPrim:HeaderDropDownControl>
    ...

Control Implementation

Filtering

The filtering logic is in the ApplyFilter() method:

protected internal void ApplyFilter()
{
    if (!this._isDirty || this._dataType == null || this._searchTextBox == null)
    {
        return;
    }

    CellBase cell = this.Cell;
    string columnKey = cell.Column.Key;
    RowsManager rm = (RowsManager)cell.Row.Manager;
    RowsFilter rf = rm.RowFiltersCollectionResolved[columnKey];

    if (rf == null)
    {
        return;
    }

    rf.Conditions.Clear();

    if (!string.IsNullOrEmpty(this._searchTextBox.Text))
    {
        ComparisonCondition compCond = new ComparisonCondition
                                       {
                                           Operator = ComparisonOperator.Contains,
                                           FilterValue = this._searchTextBox.Text
                                       };

        rf.Conditions.Add(compCond);
    }

    this._isDirty = false;
}

Cell is a dependency property bound to the HeaderCell using this code in the control template of the HeaderCellControl:

<local:ColumnSearchBoxControl
    Cell="{Binding Path=Cell, RelativeSource={RelativeSource TemplatedParent}}" ... />

The goal is to get to the RowsFilter for the current ColumnLayout.

The rest of the code sets a filter using the Filtering API of the XamGrid. Note that when ApplyFilter() is invoked all conditions are cleared and if the text from the TextBox is just an empty string a filter won't be applied.

Maybe you are wondering where does the RowsFilter come from and what is _dataType and why the filter won't be applied when it's null. This all comes from the SetupControl method which is called in OnApplyTemplate.

protected virtual void SetupControl()
{
    RowsManager rm = this.Cell.Row.Manager as RowsManager;

    if (rm != null && rm.ItemsSource != null && this.Cell.Column != null &&
        this.Cell.Column.DataType == typeof(string))
    {
        string searchTerm = string.Empty;
        this._dataType = DataManagerBase.ResolveItemType(rm.ItemsSource);

        RowsFilter rf = rm.RowFiltersCollectionResolved[this.Cell.Column.Key];

        if (rf == null)
        {
            rf = new RowsFilter(this._dataType, this.Cell.Column);
            rm.RowFiltersCollectionResolved.Add(rf);
        }
        else if (rf.Conditions.Count > 0 && rf.Conditions[0] is ComparisonCondition)
        {
            ComparisonCondition condition = (ComparisonCondition)rf.Conditions[0];

            if (condition.Operator == ComparisonOperator.Contains)
            {
                searchTerm =
                    condition.FilterValue != null
                        ? Convert.ToString(condition.FilterValue, CultureInfo.InvariantCulture)
                        : string.Empty;
            }
        }

        this.SetTextSilently(searchTerm);
        this.IsEnabled = true;
    }
    else
    {
        this._dataType = null;
        this.SetTextSilently(string.Empty);
        this.IsEnabled = false;
    }
}

_dataType stores the type of the objects in the collection used by the ChildBand where the ColumnSearchBoxControl is placed. This type is used in the RowsFilter constructor. Another thing that needs to be mentioned is that the current implementation supports only columns with DataType - string. If the column is not representing a string property the control is disabled and _dataType is set to null, which is used as flag to prevent filtering.

Commands

My goal was to allow the following behavior:

  • Clear the filters applied on the column  using "Clear" button - this was easy, the XamGrid already has this command (ClearFiltersCommand from the ColumnSearchBoxControlCommandSource).
  • Close the popup using a close button "X" and close the popup when the "Clear" button is pressed - same as above, I used ClosePopupCommand from the XamGridPopupCommandSource.
  • Apply the filter and Close the popup when Enter is pressed in the TextBox - The first part is easy… I just had to create custom command and command-source. Hm … but what about the second requirement - I can't use the ClosePopupCommand and command-source wired to KeyDown, because  it will close the popup after the first keystroke, not only when Enter is pressed.

The solution  - I created a custom command that derives from ClosePopupCommand and overrode the CanExecute method, allowing execution only when Enter is pressed.

public class ClosePopupCommandEx : ClosePopupCommand
{
    public override bool CanExecute(object parameter)
    {
        KeyEventArgs keyEventArgs = this.CommandSource.OriginEventArgs as KeyEventArgs;

        if (keyEventArgs != null)
        {
            return keyEventArgs.Key == Key.Enter;
        }

        return base.CanExecute(parameter);
    }
}

This command is returned from the Resolve method of a custom command-source derived from XamGridPopupCommandSource:

public class XamGridPopupCommandSourceEx : XamGridPopupCommandSource
{
    protected override ICommand ResolveCommand()
    {
        if (this.CommandType == XamGridPopupCommand.ClosePopup)
        {
            return new ClosePopupCommandEx();
        }

        return base.ResolveCommand();
    }
}

Other implementation notes

The ColumnSearchBoxControl has a boolean property IsIncrementalFilteringEnabled. When this property is set to true the filters will be executed after every keystroke.

// This code is executed when the text in the TextBox is changed
private void SearchTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
    ...
    if (this.IsIncrementalFilteringEnabled)
    {
        this.ApplyFilter();
    }
}

Source Code

ColumnSearchBoxSample.zip