A Rich Find And Replace Control For Almost All WPF Controls - CodeProject

:

Introduction

In this article I will describe how to use a Find/Replace Control for WPF applications. There is another WPF Find/Replace Dialog article on CodeProject, however, this article and its associated code (which was not based on the other article) will include additional features:

  • As you type match highlighting
  • Visual Studio style 'find/replace' dialog
  • MS Office style dialog
  • Scrollbar highlighting of match positions
  • 'Run' object oriented means that it supports nearly all WPF Controls, including TextBox, RichTextBox, ComboBox, DataGrid, Label, ListBox, ListView, TreeView, FlowDocumentPageViewer, FlowDocumentScrollViewer, Button (disabled by default), IContentHost Controls, Non visual collection of Run objects (ie. strings), and Controls using any of these as child controls (eg. UserControl, Window etc)
  • Ignore whitespace and ignore punctuation options

The other article and code has two advantages that I could immediately see. Firstly, it supports MDI in that it can work across multiple Views (where as this code works on one only) and secondly you may find its interface for working with 3rd party controls easier to use (although this Control also supports 3rd party controls).

This article will also explain some interesting points of the Control design.

Background

In 2014, my company developed the code for this Control because it was a unique product in the WPF component marketplace (supplementing only a few projects from articles such as the one on CodeProject). We are pleased with the design and having had some interest in exploring open source projects in the past, felt that we could reach a wider audience by sharing our code for this product openly. We hope that it is either directly useful, or that elements of it may be of use to other WPF developers, as we did need to solve some technical problems that are of general interest.

Designer based quick-start

Step 1. Get your project ready (load or create it).

Step 2. Drag RapidFindReplacePopupControl from your Toolbox to your window.

To add the controls Toolbox - right click the Toolbox and select 'Choose Items'.
In the WPF Components Tab choose 'RapidFindReplacePopupControl' & 'RapidFindReplaceControl'.

(You'll see them if you used our MSI, otherwise click browse and find the DLLs)

Step 3. Drag a TextBox Control onto your window.

Step 4. Run!

To launch RapidFindReplace while the project is running press "Ctrl-F".

Basic Usage in XAML

When you drop RapidFindReplacePopupControl onto a Window - it will automatically scope itself to search over the entire Window and can be opened with the Find ApplicationCommand (i.e. CTRL-F).

This is the code that is produced by the designer.

<Window x:Class="RapidFindReplace_Demo_CS.Views.Basic"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:RapidFindReplace="clr-namespace:Keyoti.RapidFindReplace.WPF;assembly=Keyoti4.RapidFindReplace.WPF"

        Title="Basic" Height="237" Width="576">
    <Grid>
        <RapidFindReplace:RapidFindReplacePopupControl/>
        <TextBox Text="NorthWind Inc." Height="26" Margin="96,39,10,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="0.108,-1.13"/>
        <RichTextBox Margin="96,81,10,10" RenderTransformOrigin="-0.78,-1.1" ScrollViewer.VerticalScrollBarVisibility="Visible">
            <FlowDocument>
                <Paragraph>
                    Starting in 2009 we were contracted to perform scoping and analysis for a variety of projects that NorthWind Inc. were a subcontractor for.
                </Paragraph>
                <Paragraph>
                    The implementation of the required studies, and their overarching management framework were, going forward, integrated with regards to customer objectives.  
                        A rigorous, heterogeneous framework was put in place upon which to action out requirements for deliverables.  
                        Leveraging core competencies and drilling down into stakeholder objectives we were able blue sky it and circle back in relationship to Q4.
                </Paragraph>
            </FlowDocument>
        </RichTextBox>
        <Label Content="Customer Name" HorizontalAlignment="Left" VerticalAlignment="Top" RenderTransformOrigin="-0.474,-0.308" Margin="0,37,0,0"/>
        <Menu HorizontalAlignment="Left" Height="22" VerticalAlignment="Top" Width="648">
            <MenuItem Header="_Tools">
                <MenuItem Header="_Find..." Command="Find"/>
            </MenuItem>
        </Menu>
        <Label Content="Work Perfomed" HorizontalAlignment="Left" VerticalAlignment="Top" RenderTransformOrigin="-0.474,-0.308" Margin="0,79,0,0"/>

    </Grid>
</Window>

Office Style Resource Dictionary

The Control project includes "OfficeStyle.xaml" which can be used to style the Control in a way similar to the Office find dialog. To use it add the resource dictionary, eg. in the Window XAML.

<Window.Resources>
    <ResourceDictionary Source="pack://application:,,,/Keyoti4.RapidFindReplace.WPF;component/Resources/OfficeStyle.xaml"/>
</Window.Resources>

Points of Interest

There are three main parts to the control that are interesting to look at; Highlight handling (how highlights are applied to matches inside a control), Run readers (how text is pulled from controls for matching) and Matchers and filters (how matches are found and character classes ignored). These are explained below.

Highlight handling

The Control matches and highlights as the user types (either types in to the Find dialog, or types text in a text box that matches). Highlighting is actually rendered by Adorner classes (HighlightAdorner and subclasses for certain controls).

Diagram showing 3 main groups of classes; HighlightHandlers, Highlights, and HighlightAdorners.

  1. HighlightHandler is associated with each compatible control in the Window. For certain types of controls, specialist HighlightHandler subclasses are required, these handle behavioral differences between the controls, for example, the TextBoxHighlightHandler and RichTextBoxHighlightHandler classes deal with text being entered in to the textboxes while any text is highlighted (this is necessary because otherwise the existing highlights would not reflect changes to the text).
  2. Highlight objects are created and listed by the HighlightHandler, these represent a section of text within a Run that is highlighted, they also hold one HighlightAdorner (which does the actual highlight rendering). Similar to HighlightHandler there are specialist Highlight subclasses for some controls, which control how highlights are selected, scrolled to, and how text is replaced.
  3. HighlightAdorner performs rendering for general controls and has subclasses for specific controls.

ScrollBarHighlightHandler and ScrollBarHighlightAdorner are unique in that they do not subclass as the other specialist classes do. This is because scroll-bar highlighting is markedly different to other highlighting. The scroll-bar highlighting does not highlight text, but just marks in the scroll bar the line in which a highlight exists.

Run readers

It may be initially tempting to pull plain text out of controls, scan it for matches and then apply highlighting but this will inevitably lead to problems mapping between plain text indices and real positions inside Run objects, especially with FlowDocument oriented controls. Working with the Run class is the correct way to handle text from controls, even with plain text controls such as TextBox, the text can be packaged in a Run, similarly for plain string based find/replaced (which the control can do) Run objects are a good way to handle text.

The *RunReader classes have a common interface 'IRunReader'. There is a general IRunReader called IContentHostRunReader (this is actually a class not an interface, perhaps badly named!) which is the default reader for any control that implements IContentHost (eg. TextBlock). Other specific controls have their own RunReader, which iterates Run objects out of them.

Matchers and filters

Text matchers, match filters and ignore character class

The types of matchers, filters and ignore character classes that come in to play are driven by the user's option choices. It is helpful to understand the design by considering how the options affect matching.

  • TextMatchers: standard, regular expression and wildcard matching options - these matchers take the text given to them (which may actually be different to what is in the control being checked depending on other options, see below), and report where matches occur.
  • IMatchFilter: start, end and whole word options affect whether a match reported by a text matcher should be filtered out or not. In theory it would have been possible to have written a regex to handle start/end/whole word type matching, but it was felt that this could get more complicated that need be (generating the regex, with other options taken in to account and the user's ability to write their own regex).
  • IIgnoreCharacterHandler: the user can ignore whitespace and punctuation - to support this we provide the matchers with text that has whitespace and/or punctuation removed (and similarly remove it from the query).

The TextMatchers are created and held by the Query class, and each matcher uses the filters and ignore handlers provided to it.

Controls

There are 2 primary controls, the RapidFindReplaceControl and the RapidFindReplacePopupControl which houses the RapidFindReplaceControl. The RapidFindReplaceControl can be placed directly on to your own Window or UserControl for example, but the RapidFindReplacePopupControl is probably what will be used most, it hosts the RapidFindReplaceControl and manages it popping up, dragging, resizing, and docking to edges/center of a control that it is bound to.

Viewmodel

RapidFindReplaceControl has a ViewModel property which holds the find/replace 'engine', RapidFindReplaceControlViewModel. The RapidFindReplaceControlViewModel class does the work of find and replace for the Controls, but it can also be used directly without the Controls.

For example, a TextBox is bound to the Query DependencyProperty in RapidFindReplaceControlViewModel, a KeyUp handler is used to trigger as-you-type finds and the Button fires the FindTextCommand in the view model.

This style of usage allows lower level access and the opportunity to build a Find/Replace UI from scratch. The RapidFindReplaceControlViewModel declaration below also sets some brush style properties by way of example.

<Window x:Class="RapidFindReplace_Demo_CS.Views.ViewModelUsage"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:RapidFindReplace="clr-namespace:Keyoti.RapidFindReplace.WPF;assembly=Keyoti4.RapidFindReplace.WPF"

        Title="ViewModelUsage" Height="317" Width="500">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="0*"/>
        </Grid.ColumnDefinitions>
        <UserControl Height="32" VerticalAlignment="Top" HorizontalAlignment="Left" Width="155" >
            <UserControl.DataContext>
                <RapidFindReplace:RapidFindReplaceControlViewModel>
                    <RapidFindReplace:RapidFindReplaceControlViewModel.BodyHighlightAdornerBrush>
                        <SolidColorBrush Color="Yellow" Opacity=".3"/>
                    </RapidFindReplace:RapidFindReplaceControlViewModel.BodyHighlightAdornerBrush>
                    <RapidFindReplace:RapidFindReplaceControlViewModel.BodyHighlightAdornerPen>
                        <Pen Brush="DarkSlateGray" Thickness=".9"/>
                    </RapidFindReplace:RapidFindReplaceControlViewModel.BodyHighlightAdornerPen>
                </RapidFindReplace:RapidFindReplaceControlViewModel>
                    
            </UserControl.DataContext>
            <StackPanel Grid.Row="0"  Orientation="Horizontal" HorizontalAlignment="Left">
                <TextBox RapidFindReplace:RapidFindReplaceControl.IsFindable="false" x:Name="_searchTextBox" Text="{Binding Query, Converter={x:Static RapidFindReplace:ConverterInstances.QueryConverter}, UpdateSourceTrigger=PropertyChanged}" MinWidth="100" Margin="2" KeyUp="_searchTextBox_KeyUp" >
                </TextBox>
                <Button x:Name="_searchButton" Height="{Binding ActualHeight, ElementName=_searchTextBox}" Content="Find" Command="{Binding FindTextCommand}" CommandParameter="{Binding ElementName=_searchTextBox, Path=Text}">
                </Button>
            </StackPanel>
        </UserControl>
            
        <RichTextBox Margin="0,37,0,2" ScrollViewer.VerticalScrollBarVisibility="Auto" RapidFindReplace:RapidFindReplaceControl.IsFindable="true">         
        </RichTextBox>
    </Grid>
</Window>

You can also find latest source and branches on GitHub.

History

Version 1.0.0 - Initial release.