Question

I am trying to implement a search (Ctrl+F) feature on a XamDataGrid. How can I programmatically invoke record filtering on the grid that searches across content in multiple columns and displays only columns that match the search?

Was it helpful?

Solution

The record filtering in the DataPresenter is just that - a means of the filtering the records based on some specified criteria. Normally that criteria is provided via one of the built in ui's - either using the LabelIcons which is just a drop down list of the filtered values or via the FilterRecord which is a dedicated special record that is displayed with cells for each column to allow choosing/entering an operator and value.

That being said the record filtering can be manipulated in code if you wish. The FieldLayout exposes a RecordFilters collection where a RecordFilter provides the conditions (i.e. the match criteria) and the field for which the match should be performed. It is also exposed off the RecordManager but this is really to facilitate filtering in hierarchical situations where the filter criteria is different for each "island" of child records.

Since you want to search multiple fields for the same criteria, you would need to create a RecordFilter for each Field in the FieldLayout's Fields collection (or whatever subset of the Fields to which you want this to be applied). Since you want to do a text search you probably want to use a ComparisonCondition where the ComparisonOperator you use is Contains and the value would be the text to search for. Now since you want a record to match if the value is found in any of the fields (for which you have created a RecordFilter) you will also need to set the FieldLayoutSettings' RecordFiltersLogicalOperator property to Or (by default this resolves to And since one typically wants to match a record when all the criteria matches the entered values).

So to that end the following is a basic attached behavior (in this case a property named FilterText that you would set on the DataPresenter to be filtered). This behavior/property will manipulate the RecordFilters of the DefaultFieldLayout considering the text value of the FilterText property.

public static class DataPresenterHelpers
{
    #region FilterText

    /// <summary>
    /// FilterText Attached Dependency Property
    /// </summary>
    public static readonly DependencyProperty FilterTextProperty =
        DependencyProperty.RegisterAttached("FilterText", typeof(string), typeof(DataPresenterHelpers),
            new FrameworkPropertyMetadata((string)null,
                new PropertyChangedCallback(OnFilterTextChanged)));

    /// <summary>
    /// Gets the text to be used to filter the DataPresenter on which the property was set.
    /// </summary>
    public static string GetFilterText(DependencyObject d)
    {
        return (string)d.GetValue(FilterTextProperty);
    }

    /// <summary>
    /// Sets the filter text on the DataPresenter that should be used to manipulate the RecordFilters of the specified DataPresenter
    /// </summary>
    public static void SetFilterText(DependencyObject d, string value)
    {
        d.SetValue(FilterTextProperty, value);
    }

    /// <summary>
    /// Handles changes to the FilterText property.
    /// </summary>
    private static void OnFilterTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dp = d as DataPresenterBase;

        if (dp.DefaultFieldLayout != null)
        {
            dp.DefaultFieldLayout.RecordFilters.Clear();
            dp.DefaultFieldLayout.Settings.RecordFiltersLogicalOperator = LogicalOperator.Or;

            foreach (var field in dp.DefaultFieldLayout.Fields)
            {
                var filter = new RecordFilter();
                filter.Field = field;
                filter.Conditions.Add(new ComparisonCondition(ComparisonOperator.Contains, e.NewValue));
                dp.DefaultFieldLayout.RecordFilters.Add(filter);
            }
        }
    }

    #endregion //FilterText
}

You can then do something like the following to hook up the value of a TextBox to this attached property. Note, you would need to define an xmlns mapping for local to be whatever clr namespace you put the above class into.

    <TextBox DockPanel.Dock="Top" x:Name="txtFilter" />
    <igDP:XamDataGrid 
        x:Name="grid" 
        BindToSampleData="True" 
        local:DataPresenterHelpers.FilterText="{Binding ElementName=txtFilter, Path=Text}">
    </igDP:XamDataGrid>

Now the other part of your question was about only showing the columns which contained matching values. This isn't really possible as part of the record filters itself since record filtering is about filtering out records based on some specified criteria and has no relation to hiding/showing columns/fields. That being said perhaps one way to help the end user understand where the matching text resides would be to highlight that text within the cells.

To provide such functionality I defined a derived TextBlock called HighlightTextBlock. It exposes several properties:

  • RawText - This is the source text that will be displayed. It cannot use the Text property because this element will manipulate the Inlines of the TextBlock and that will set the Text property which would break bindings in the case of one way bindings or push back values to the source in the case of two way bindings.
  • FilterText - This is used to indicate the text that is to be sought within the RawText.
  • FilterTextComparisonType - This is used to indicate the string comparison for the match (i.e. case sensitive, etc.).
  • FilterTextForeground - The foreground to be used to highlight the matching text.
  • FilterTextBackground - The background to be used to highlight the matching text.

Here is the code for the class:

    public class HighlightTextBlock 
    : TextBlock
{
    #region Member Variables

    private DispatcherOperation _pendingUpdate; 

    #endregion //Member Variables

    #region Constructor
    static HighlightTextBlock()
    {
    }

    /// <summary>
    /// Initializes a new <see cref="HighlightTextBlock"/>
    /// </summary>
    public HighlightTextBlock()
    {

    } 
    #endregion //Constructor

    #region Base class overrides

    #region OnInitialized
    protected override void OnInitialized(EventArgs e)
    {
        if (_pendingUpdate != null)
            this.UpdateInlines(null);

        base.OnInitialized(e);
    }
    #endregion //OnInitialized 

    #endregion //Base class overrides 

    #region Properties

    #region FilterText

    /// <summary>
    /// Identifies the <see cref="FilterText"/> dependency property
    /// </summary>
    public static readonly DependencyProperty FilterTextProperty = DependencyProperty.Register("FilterText",
        typeof(string), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCriteriaChanged)));

    /// <summary>
    /// Returns or sets the text that should be highlighted
    /// </summary>
    /// <seealso cref="FilterTextProperty"/>
    [Description("Returns or sets the text that should be highlighted")]
    [Category("Behavior")]
    [Bindable(true)]
    public string FilterText
    {
        get
        {
            return (string)this.GetValue(HighlightTextBlock.FilterTextProperty);
        }
        set
        {
            this.SetValue(HighlightTextBlock.FilterTextProperty, value);
        }
    }

    #endregion //FilterText

    #region FilterTextBackground

    /// <summary>
    /// Identifies the <see cref="FilterTextBackground"/> dependency property
    /// </summary>
    public static readonly DependencyProperty FilterTextBackgroundProperty = DependencyProperty.Register("FilterTextBackground",
        typeof(Brush), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(Brushes.Yellow, new PropertyChangedCallback(OnCriteriaChanged)));

    /// <summary>
    /// Returns or sets the background of the matching text.
    /// </summary>
    /// <seealso cref="FilterTextBackgroundProperty"/>
    [Description("Returns or sets the background of the matching text.")]
    [Category("Behavior")]
    [Bindable(true)]
    public Brush FilterTextBackground
    {
        get
        {
            return (Brush)this.GetValue(HighlightTextBlock.FilterTextBackgroundProperty);
        }
        set
        {
            this.SetValue(HighlightTextBlock.FilterTextBackgroundProperty, value);
        }
    }

    #endregion //FilterTextBackground

    #region FilterTextComparisonType

    /// <summary>
    /// Identifies the <see cref="FilterTextComparisonType"/> dependency property
    /// </summary>
    public static readonly DependencyProperty FilterTextComparisonTypeProperty = DependencyProperty.Register("FilterTextComparisonType",
        typeof(StringComparison), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(StringComparison.CurrentCultureIgnoreCase, new PropertyChangedCallback(OnCriteriaChanged)));

    /// <summary>
    /// Returns or sets the StringComparison when locating the FilterText within the RawText.
    /// </summary>
    /// <seealso cref="FilterTextComparisonTypeProperty"/>
    [Description("Returns or sets the StringComparison when locating the FilterText within the RawText.")]
    [Category("Behavior")]
    [Bindable(true)]
    public StringComparison FilterTextComparisonType
    {
        get
        {
            return (StringComparison)this.GetValue(HighlightTextBlock.FilterTextComparisonTypeProperty);
        }
        set
        {
            this.SetValue(HighlightTextBlock.FilterTextComparisonTypeProperty, value);
        }
    }

    #endregion //FilterTextComparisonType

    #region FilterTextForeground

    /// <summary>
    /// Identifies the <see cref="FilterTextForeground"/> dependency property
    /// </summary>
    public static readonly DependencyProperty FilterTextForegroundProperty = DependencyProperty.Register("FilterTextForeground",
        typeof(Brush), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnCriteriaChanged)));

    /// <summary>
    /// Returns or sets the brushed used for the foreground of the matching text.
    /// </summary>
    /// <seealso cref="FilterTextForegroundProperty"/>
    [Description("Returns or sets the brushed used for the foreground of the matching text.")]
    [Category("Behavior")]
    [Bindable(true)]
    public Brush FilterTextForeground
    {
        get
        {
            return (Brush)this.GetValue(HighlightTextBlock.FilterTextForegroundProperty);
        }
        set
        {
            this.SetValue(HighlightTextBlock.FilterTextForegroundProperty, value);
        }
    }

    #endregion //FilterTextForeground

    #region RawText

    /// <summary>
    /// Identifies the <see cref="RawText"/> dependency property
    /// </summary>
    public static readonly DependencyProperty RawTextProperty = DependencyProperty.Register("RawText",
        typeof(string), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCriteriaChanged)));

    /// <summary>
    /// Returns or sets the base string that will be displayed by the element.
    /// </summary>
    /// <seealso cref="RawTextProperty"/>
    [Description("Returns or sets the base string that will be displayed by the element.")]
    [Category("Behavior")]
    [Bindable(true)]
    public string RawText
    {
        get
        {
            return (string)this.GetValue(HighlightTextBlock.RawTextProperty);
        }
        set
        {
            this.SetValue(HighlightTextBlock.RawTextProperty, value);
        }
    }

    #endregion //RawText

    #endregion //Properties

    #region Methods

    #region OnCriteriaChanged
    private static void OnCriteriaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var instance = d as HighlightTextBlock;

        if (instance._pendingUpdate == null)
        {
            instance._pendingUpdate = instance.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new SendOrPostCallback(instance.UpdateInlines), new object[] { null });
        }
    }
    #endregion //OnCriteriaChanged

    #region UpdateInlines
    private void UpdateInlines(object param)
    {
        _pendingUpdate = null;

        string filterText = this.FilterText;
        string text = this.RawText;
        var inlines = this.Inlines;

        try
        {
            inlines.Clear();

            if (string.IsNullOrEmpty(filterText))
            {
                inlines.Add(text);
                return;
            }

            var foreground = this.FilterTextForeground;
            var background = this.FilterTextBackground;
            var comparison = this.FilterTextComparisonType;
            var newInlines = new List<Inline>();
            int filterTextLen = filterText.Length;

            int start = 0;

            do
            {
                int end = text.IndexOf(filterText, start, comparison);

                string substr = text.Substring(start, (end < 0 ? text.Length : end) - start);
                newInlines.Add(new Run(substr));

                if (end < 0)
                    break;

                var run = new Run(text.Substring(end, filterTextLen));

                // note we could bind and not rebuild when the background/foreground 
                // changes but that doesn't seem likely to happen and would add more 
                // overhead than just referencing the value directly
                if (null != foreground)
                    run.Foreground = foreground;

                if (null != background)
                    run.Background = background;

                newInlines.Add(run);

                start = end + filterTextLen;
            } while (true);

            inlines.AddRange(newInlines);
        }
        finally
        {
            if (_pendingUpdate != null)
            {
                _pendingUpdate.Abort();
                _pendingUpdate = null;
            }
        }
    }
    #endregion //UpdateInlines

    #endregion //Methods
}

So then you could change the templates for the editors you are using to use this in their render template. e.g.

<Window x:Class="WpfApplication6.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:igDP="http://infragistics.com/DataPresenter"
    xmlns:igEditors="http://infragistics.com/Editors"
    xmlns:local="clr-namespace:WpfApplication6"
    Title="MainWindow" Height="350" Width="525">
<DockPanel>
    <TextBox DockPanel.Dock="Top" x:Name="txtFilter" />
    <igDP:XamDataGrid 
        x:Name="grid" 
        BindToSampleData="True" 
        local:DataPresenterHelpers.FilterText="{Binding ElementName=txtFilter, Path=Text}">
        <igDP:XamDataGrid.Resources>
            <Style TargetType="igEditors:XamTextEditor">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="igEditors:XamTextEditor">
                            <Border x:Name="MainBorder"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                >
                                <local:HighlightTextBlock 
                                    Margin="{TemplateBinding Padding}"
                                    FilterText="{Binding Path=Host.DataPresenter.(local:DataPresenterHelpers.FilterText), RelativeSource={RelativeSource TemplatedParent}}"
                                    RawText="{TemplateBinding DisplayText}" 
                                    TextWrapping="{TemplateBinding TextWrapping}" 
                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                    TextAlignment="{TemplateBinding TextAlignmentResolved}"
                                  />
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </igDP:XamDataGrid.Resources>
    </igDP:XamDataGrid>
</DockPanel>

OTHER TIPS

The way that I would approach this problem would be to add code to the search button click event (or some other trigger mechanism) and perform the search directly in code, using linq or record by record comparison, whatever works best in your situation (it would help to know how the columns and rows are populated).

Once you have the set of records and/or columns that have the appropriate results, hide the columns that do not have hits. To streamline the process, I would create a collection of columns that should be hidden, then have a method that processes each column. The method would search through the column data and at the sign of the first match, would remove the column from the list of columns to be hidden and stop processing records in the column.

At the end of the process, your list would contain the list of columns to hide, which you could then use to perform the actual hiding.

This mechanism could also be extended to hide rows that don't have any matches, assuming that there are multiple rows. I would support this by hiding each of the rows that don't have a match while processing the first column and then unhiding them for subsequent columns where a match was found.

Thank you for sharing. I'll share too. I used this example to handle a more simple case. I just wanted to set up a filter on one column. Here it is:

Imports Infragistics.Windows.DataPresenter
Imports Infragistics.Windows.Controls

Public Class DataPresenterFilter

Public Shared Function GetTitleFilter(ByVal element As DependencyObject) As String
    If element Is Nothing Then
        Throw New ArgumentNullException("element")
    End If
    Return element.GetValue(TitleFilter)
End Function

Public Shared Sub SetTitleFilter(ByVal element As DependencyObject, 
        ByVal value As String)
    If element Is Nothing Then
        Throw New ArgumentNullException("element")
    End If
    element.SetValue(TitleFilter, value)
End Sub

Public Shared ReadOnly TitleFilter As  _
        DependencyProperty = DependencyProperty.RegisterAttached("TitleFilter", _
    GetType(String), GetType(DataPresenterFilter), _
    New FrameworkPropertyMetadata(String.Empty, 
    New PropertyChangedCallback(AddressOf OnTitleFilterChanged)))

Private Shared Sub OnTitleFilterChanged(d As DependencyObject, 
        e As DependencyPropertyChangedEventArgs)
    Dim dp As DataPresenterBase = CType(d, DataPresenterBase)

    If (Not dp.DefaultFieldLayout Is Nothing) Then
        Dim Filter As RecordFilter = New RecordFilter()
        Filter.FieldName = "Title"
        Filter.Conditions.Add(
            New ComparisonCondition(ComparisonOperator.Contains, e.NewValue))
        dp.DefaultFieldLayout.RecordFilters.Remove(
            dp.DefaultFieldLayout.RecordFilters.Item("Title"))
        dp.DefaultFieldLayout.RecordFilters.Add(Filter)
    End If
End Sub

End Class

And the XAML:

xmlns:Inv="clr-namespace:InventoryApp"

<TextBox Height="23" Margin="0,2,0,2" x:Name="tbTitle" />

<igDP:XamDataPresenter Name="xamDataPresenterPublicationCollection" 
    DataSource="{Binding Source={StaticResource PublicationCollectionViewSource},
    UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"
    ActiveDataItem="{Binding Path=PublicationModel, Mode=OneWay}" 
    Inv:DataPresenterFilter.TitleFilter="{Binding ElementName=tbTitle, Path=Text}"/>

This works perfectly for me.

I was looking for a very basic solution, so I'll share what has worked for me. Here is my C# code-behind code to apply a filter to a column:

grdConnectionManagerEntries.FieldLayouts[0].RecordFilters.Clear();  // Clear any existing filters before applying this one.
grdConnectionManagerEntries.FieldLayouts[0].RecordFilters.Add(new RecordFilter(new Field("Name")));
grdConnectionManagerEntries.FieldLayouts[0].RecordFilters[0].Conditions.Add(new Infragistics.Windows.Controls.ComparisonCondition(Infragistics.Windows.Controls.ComparisonOperator.Contains, connectionName));

Here grdConnectionManagerEntries is my XamDataGrid, I have a Field (i.e. column) called Name, and I'm adding a Contains filter to it that will filter by the given connectionName.

For some reason the filter text only shows up in the XamDataGrid's UI when I use a Contains comparison operator; I'm guessing because that's the default filter operator on that column. So there's something that I'm not doing quite right, but since I wanted to use the Contains operator anyways it has worked for my purposes.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top