Creating Advanced Filter UIs in WPF Using FilterEditor and DataFilter
In the 2020v2 release of ComponentOne we added a FilterEditor control for WPF, which provides an intuitive UI for filtering data sources at run time. This post demonstrates how we can integrate the FilterEditor for WPF with the C1DataFilter control to create rule-based filtering for an e-mail client application. Using the filter editor to create rule-based filtering, we'll see how a user can break down a large data set (i.e., the list of e-mails here) into a hierarchical tree of logically formed nodes. Each node corresponds to smaller, related, and more manageable datasets.
In this blog, we will first have a quick look at the FilterEditor structure. After that, we will cover
- Creating the models required for the MailBox.
- Creating the RuleEditor using the FilterEditor control.
- Creating the RuleTree to be displayed in the DataFilter.
- Integrating the RuleTree with a custom filter (named RuleBasedFilter) for the DataFilter.
- Adding the custom filter to the DataFilter control.
FilterEditor for WPF: A Review of the Structure
The following image shows what the FilterEditor control looks like:
It displays a tree of filter nodes where each node represents either a Combination or an Operation expression. These are the building blocks for creating any filter criteria for our data source at runtime. You can read more about the FilterEditor structure from the documentation and this blog. Next, let's look at the mailbox that we'll be creating.
How to Build the MailBox
In this post, we will create a Gmail mailbox, where logging in and fetching e-mails is done using MailKit and Gmail APIs.
We will not review the details for these as they are out of scope for this blog. The source code for this can be found in the sample attached at the end of this blog.
Creating the Models: Message and Rule
The MailBox we'll be creating will have the following two models:
- Message:
An e-mail message will have the following object model:
public class Message
{
public string From { get; set; }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool SendByMe { get; set; }
public DateTime? Date { get; set; }
}
The FilterEditor will be using these properties for defining the filter expressions.
- Rule:
A Rule represents the filter criteria that the user would define in the MailBox at runtime. These rules are translated into Expressions using the 'Message' model we described above. For example, a rule that says filter "All e-mails from Amazon" will be translated to the following expression:
'[Message.From] Contains [amazon]'
We will be using the FilterEditor to define rules similar to the above. Once these rules are defined, they'll be represented by a node in our RuleTree.
The object model for the Rule is as follows:
public class Rule: INotifyPropertyChanged
{
public string Name { get { return _name; } set { _name = value; OnPropertyChanged(nameof(Name)); } }
public bool IsExpanded { get { return _isExpanded; } set { _isExpanded = value; OnPropertyChanged(nameof(IsExpanded)); } }
public bool IsSelected { get { return _isSelected; } set { _isSelected = value; OnPropertyChanged(nameof(IsSelected)); } }
public bool CanDelete { get; set; } = true;
public Expression Expression { get; set; }
public ObservableCollection<Rule> ChildRules { get { return _child; } }
public Rule ParentRule { get; set; }
public Expression CombinedExpression
{
get
{
var combination = new CombinationExpression() { FilterCombination = FilterCombination.And };
if (this.Expression != null)
combination.Expressions.Add(this.Expression);
if (this.ParentRule?.Expression != null)
combination.Expressions.Add(this.ParentRule?. CombinedExpression);
return combination;
}
}
}
Since the rules are to be displayed in a hierarchical structure, filtering on a child node needs to consider the rules defined at its parent node. Filtering is performed with the 'CombinedExpression' property (explained above), which combines the expression for a given Rule with its Parent. The actual e-mail filtering is done based on the value of the CombinedExpression.
Creating the Rule Editor
The RuleEditor is an essential part of our MailBox application and is used to create new rules or edit existing ones. Displayed as a popup, it is created using the C1FilterEditor control to define expressions for our rules. It consists of two parts:
- Rule Name: The name of the rule which depicts a node in the Rule Tree.
- Rule Expression: The expression that defines logical criteria for a Rule using the FilterEditor UI.
The following snippet shows the code that defines the RuleEditor:
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Label VerticalAlignment="Center" Foreground="{DynamicResource foreground}" Content="Rule Name:"/>
<TextBox Text="{Binding Name}" Margin="10" Width="300"/>
</StackPanel>
<c1:C1FilterEditor x:Name="filterEditor" Grid.Row="2" Loaded="FilterEditor_Loaded"/>
And this is how the 'Amazon Mails' rule that we saw earlier is created using the FilterEditor:
For the FilterEditor to populate the list of properties on which expressions are built, we need to set its ItemSource. Similarly, to edit the Expressions from a Rule, we will set the FilterEditor's Expression property, as shown below:
public RuleEditorControl(Rule rule, IEnumerable source)
{
InitializeComponent();
this.Rule = rule;
this.DataContext = Rule;
filterEditor.Expression = Rule.Expression;
filterEditor.ItemsSource = source;
}
All of this takes care of our FilterEditor based RuleEditor. Lastly, we need to notify the DataFilter of any changes in the rule expressions. For this, we will handle the clicks on Apply/ResetFilter buttons to update the Expression and trigger the RuleChanged event. This event will be handled in the RuleTree later:
private void FilterEditor_Loaded(object sender, RoutedEventArgs e)
{
var applyButton = filterEditor.Template.FindName("Apply", filterEditor) as Button;
var resetButton = filterEditor.Template.FindName("Reset", filterEditor) as Button;
applyButton.PreviewMouseDown += (ss, ee) => { OnRuleChanged(); };
resetButton.PreviewMouseDown += (ss, ee) => { OnRuleChanged(); };
}
private void OnRuleChanged()
{
Rule.Expression = filterEditor.Expression;
RuleChanged?.Invoke(this, EventArgs.Empty);
}
Creating the Rule Tree
The RuleTree serves as the custom filter in the DataFilter used in the mailbox. It serves two purposes:
- It serves as a container to display the list of Rules defined in the MailBox. As these Rules can be hierarchical, RuleTree is created using the C1TreeView control, where each node represents a defined Rule. Each node of the tree displays the' Name' of the rule and buttons to add/edit the Rule. These buttons are used to invoke the RuleEditor.
- Since it is part of the DataFilter, it allows e-mail filtering in the mailbox using the Rules defined using the FilterEditor. The RuleTree does this by raising its own custom 'RuleChanged' event when the selected rule/node changes to notify the DataFilter of this change.
private void RuleTree_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ruleTree.SelectedItem!=null)
{
RuleChanged?.Invoke(this, EventArgs.Empty);
}
}
The expression on which the DataFilter control will filter the e-mails is exposed by the RuleTree as follows:
public C1.DataFilter.Expression Expression
{
get
{
return (ruleTree.SelectedItem?.DataContext as Rule)?.CombinedExpression;
}
}
In the next section, we will see how a custom filter is created for the DataFilter using this RuleTree.
Integrating the RuleTree with a Custom Filter for the DataFilter
DataFilter provides support for the use of custom filters with any UI component. Since we need to use our RuleTree as a means for filtering, we need to define a custom filter (viz. RuleBasedFilter) which uses RuleTree as its control:
public class RuleBasedFilter : CustomFilter
{
RuleTree _ruleTree;
public RuleBasedFilter(IEnumerable source) : base()
{
Control = _ruleTree = new RuleTree() { Source = source};
_ruleTree.RuleChanged += (s, e) =>
{
OnValueChanged(new ValueChangedEventArgs() { ApplyFilter = true });
};
}
protected override C1.DataFilter.Expression GetExpression()
{
return _ruleTree.Expression;
}
public override bool IsApplied => _ruleTree.Expression!=null;
}
The custom filter is defined in three steps after inheriting from CustomFilter:
- Specify the UI element (i.e., RuleTree in this case) using its Control property.
- Override GetExpression() to return the filter expression. We use RuleTree.Expression here.
- Notify any change in the filter criteria by subscribing to the RuleTree.RuleChanged event.
Adding the Custom Filter to the DataFilter Control
At this stage, all the required components for the mailbox are ready. The last layer that remains is, initializing the DataFilter, binding it to our e-mails, and integrating it with the custom filter created above, i.e., RuleBasedFilter. The following snippets show how it is done:
<c1:C1DataFilter x:Name="dataFilter" ItemsSource="{Binding Messages}" AutoGenerateFilters="False"/>
private void InitFilters()
{
dataFilter.Filters.Add(new RuleBasedFilter(UserService.Messages));
}
Let us have a look into the functioning app now:
This last layer brings us to the end of this blog. We learned how to enable item filtering in any application using the FilterEditor and DataFilter controls.
The complete sample is available here.
You can read more about these components from their respective documentations: