Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

A Universal WPF Find / Replace Dialog

Rate me:
Please Sign up or sign in to vote.
4.92/5 (18 votes)
14 Sep 2013CPOL4 min read 119.3K   3.8K   63   45
A WPF Find/Replace Manager, usable with most text editors and both SDI and MDI interfaces

Introduction 

This article describes a WPF Find/Replace dialog, which is designed to work with various types of text editor controls, in particular the AvalonEdit TextEditor. Furthermore, it supports multiple document interfaces. Here is a screenshot:

WPFFindReplace/FRM_screenshot.png

Using the Code (SDI & Code Behind Example)

For a single document editor, the Find/Replace dialog can be used as follows:

C#
FindReplace.FindReplaceMgr FRM = new FindReplace.FindReplaceMgr();
public MainWindow()
{
    InitializeComponent();
    FRM.CurrentEditor = new FindReplace.TextEditorAdapter(MyTextEditor);
    FRM.ShowSearchIn = false;
    FRM.OwnerWindow = this;

    CommandBindings.Add(FRM.FindBinding);
    CommandBindings.Add(FRM.ReplaceBinding);
    CommandBindings.Add(FRM.FindNextBinding);
}

Here, it is assumed that you are using an AvalonEdit TextEditor control with name MyTextEditor.

Using the Code (MDI & XAML Example)

Next, let us consider the multiple document (MDI) case. Usually, there will be a list of views, together with a current view object. FindReplaceMgr can be instantiated in XAML as follows:

XML
<Window.Resources>
   <my:MyViewData x:Key="ViewData" />
   <FR:IEditorConverter x:Key="IEC" />
   <FR:FindReplaceMgr x:Key="FRep" InterfaceConverter="{StaticResource IEC}" 
     Editors="{Binding Source={StaticResource ViewData}, Path=Views}"
     CurrentEditor="{Binding Source={StaticResource ViewData}, 
	Path=ActiveView, Mode=TwoWay}" />
</Window.Resources> 

Here, I assume that MyViewData is a class which contains a list of view objects Views and a view object CurrentView. The member InterfaceConverter of the FindReplaceMgr contains a converter, which takes a view object, and returns a FindReplace.IEditor interface, through which the FindReplaceMgr accesses the view. The built-in converter FindReplace.IEditorConverter accepts "the usual" editor controls. If you have a custom view class, you have to write a custom converter, but this is not much work, see below.

Abstracting a Text Editor Control

I wanted the Find Replace manager to be independent from any specific editor control implementation. Hence, all access to the editor control is channeled through the following interface:

C#
public interface IEditor
{
    string Text { get; }
    int SelectionStart { get; }
    int SelectionLength { get; }
    void Select(int start, int length);
    void Replace(int start, int length, string ReplaceWith);
    void BeginChange();
    void EndChange();
}

The interface members are more or less self-explanatory. The BeginChange and EndChange methods are called before and after a replace all operation, allowing the editor to handle all replacements as a single undo group. There are predefined adapters for the AvalonEdit TextEditor, the WPF RichTextBox, the Windows Forms TextBoxBase, and the WPF TextBox (for the latter one, see the remark below). For example, in the SDI case, you can wire a WPF RichTextBox to the FindReplaceMgr with the following one-liner:

C#
FRM.CurrentEditor = new RichTextBoxAdapter(MyRichTextBox);

MDI and Compatibility (more or less) with MVVM

My goal was to be able to integrate the FindReplaceMgr with few lines of code into "the typical" MVVM based MDI application. Such an application has typically a List of views somewhere, together with a variable specifying the current (active) view. Issues to be taken into account are the following:

  • The FindReplaceMgr must be able to change the active view while searching through multiple documents.
  • The FindReplaceMgr must know the active view to search in the right document.
  • The view objects are typically of some user defined types.
  • The rest of the application should not need to "know about" FindReplaceMgr (i.e., no changes to the view or view model should be necessary.)

My solution is as follows: The FindReplaceMgr has dependency properties Editors and CurrentEditor, which are to be bound to the view list and active view object. Here, the views can be any object (no special type assumed). When the view needs to be accessed for searching, FindReplaceMgr does the following:

  1. It checks whether the CurrentEditor implements the IEditor interface. If it does, it is accessed through this interface.
  2. Otherwise, it resorts to its InterfaceConverter member to convert the view object into an IEditor interface.

Hence, if you have custom view objects, you have two choices:

  1. Implement IEditor in your view.
  2. Write a custom converter. Usually, this amounts to just instantiating a pre-defined adapter on the editor control residing in your view object.

For example, the built-in converter looks as follows:

C#
public class IEditorConverter : IValueConverter
{
    object IValueConverter.Convert(object value, Type targetType, object
               parameter, CultureInfo culture)
    {
        if (value is TextEditor)
            return new TextEditorAdapter(value as TextEditor);
        else if (value is TextBox)
            return new TextBoxAdapter(value as TextBox);
        else if (value is RichTextBox)
            return new RichTextBoxAdapter(value as RichTextBox);
        else if (value is WindowsFormsHost)
            return new WFTextBoxAdapter(
                  (value as WindowsFormsHost).Child 
                      as System.Windows.Forms.TextBoxBase);
        else return null;
    }

    object IValueConverter.ConvertBack(object value, Type targetType,
           object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Handling Commands

Typically, an application providing find/replace functionality binds to ApplicationCommands.Find, etc. I wanted this to be doable without having the user of FindReplaceMgr write command handlers and proxy the call to FindReplaceMgr. My solution is to expose three CommandBindings as properties of FindReplaceMgr, namely:

FindBindingBinds to ApplicationCommands. Find and opens the Find Replace dialog.
ReplaceBindingBinds to ApplicationCommands. Replace and opens the Find Replace dialog, with Replace tab active.
FindNextBindingBinds to NavigationCommands.Search (default shortcut: F3) and searches for the next match. Note: If any non-null command parameter is provided, the search is conducted in reverse direction.

For example, you can wire up the commands to using the following XAML code (for the alternative version in C# code, see the SDI example above):

XML
<Window.CommandBindings>
    <my:StaticResourceEx ResourceKey="FRep" Path="FindBinding" />
    <my:StaticResourceEx ResourceKey="FRep" Path="ReplaceBinding" />
    <my:StaticResourceEx ResourceKey="FRep" Path="FindNextBinding" />
</Window.CommandBindings>
<Window.InputBindings>
    <KeyBinding Key="F3" Modifiers="Shift" Command="Search" 
	CommandParameter="something" />
</Window.InputBindings>

Here, I use the markup extension StaticResourceEx from here to bind to the resource's members.

Remarks About the Native WPF Text Editors

The native WPF text editors TextBox and RichTextBox are not so easy to use with a find replace dialog, since they do not have a HideSelection property. The workarounds I found on the web did not work in the present setup. (Here, the focus is stolen by another window instead of another control on the same window.) The RichTextBox adapter circumvents this by coloring the currently selected text yellow. Here is the code sample:

C#
public class RichTextBoxAdapter : IEditor
{
    ...
    TextRange oldsel = null;
    public void Select(int start, int length)
    {
        TextPointer tp = rtb.Document.ContentStart;
        rtb.Selection.Select(GetPoint(tp, start), GetPoint(tp, start + length));
        rtb.ScrollToVerticalOffset(rtb.Selection.Start.GetCharacterRect
			(LogicalDirection.Forward).Top);
        rtb.Selection.ApplyPropertyValue(TextElement.BackgroundProperty, 
			Brushes.Yellow);
        oldsel = new TextRange(rtb.Selection.Start, rtb.Selection.End);
        rtb.SelectionChanged += rtb_SelectionChanged;            
    }

    void rtb_SelectionChanged(object sender, RoutedEventArgs e)
    {
        oldsel.ApplyPropertyValue(TextElement.BackgroundProperty, null);
        rtb.SelectionChanged -= rtb_SelectionChanged;
    }
    ...
}

For the TextBox this is not possible, and I do not know how to circumvent, except the lazy solution: Use another control type, e.g., AvalonEdit (highly recommended) or the Windows Forms TextBox.

Some Licensing Issues

Note that the AvalonEdit TextEditor, and the library ICSharpCode.AvalonEdit.dll that I provide with the download for convenience are protected by the GNU Lesser General Public License (LGPL), and are explicitly NOT distributed under the CodeProject Open License. If you use these files in your project, you have to observe the restrictions imposed by the LGPL. However, note that the class FindReplaceMgr does not use, and contains no reference to the aforementioned library.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionA Find and Replace tool for AvalonEdit Pin
Bruce Greene4-May-14 5:52
Bruce Greene4-May-14 5:52 
QuestionWhy use Dependency Properties in the Dialog and not INotifyPropertyChanged Pin
Camuvingian19-Sep-13 9:53
Camuvingian19-Sep-13 9:53 
AnswerRe: Why use Dependency Properties in the Dialog and not INotifyPropertyChanged Pin
Thomas Willwacher19-Sep-13 11:30
Thomas Willwacher19-Sep-13 11:30 
GeneralRe: Why use Dependency Properties in the Dialog and not INotifyPropertyChanged Pin
Camuvingian19-Sep-13 11:50
Camuvingian19-Sep-13 11:50 
SuggestionShould F3 work in the dialog? Pin
Chuck_Esterbrook14-Sep-13 18:06
Chuck_Esterbrook14-Sep-13 18:06 
GeneralRe: Should F3 work in the dialog? Pin
Thomas Willwacher15-Sep-13 8:06
Thomas Willwacher15-Sep-13 8:06 
QuestionLooking better Pin
Chuck_Esterbrook14-Sep-13 18:05
Chuck_Esterbrook14-Sep-13 18:05 
AnswerRe: Looking better Pin
Thomas Willwacher15-Sep-13 0:00
Thomas Willwacher15-Sep-13 0:00 
QuestionSelect all text in the "Text to Find" text box Pin
Chuck_Esterbrook12-Sep-13 21:43
Chuck_Esterbrook12-Sep-13 21:43 
AnswerRe: Select all text in the "Text to Find" text box Pin
Thomas Willwacher14-Sep-13 5:52
Thomas Willwacher14-Sep-13 5:52 
QuestionShift+F3 or other key combo for searching backwards? Pin
Chuck_Esterbrook12-Sep-13 21:41
Chuck_Esterbrook12-Sep-13 21:41 
AnswerRe: Shift+F3 or other key combo for searching backwards? Pin
Thomas Willwacher14-Sep-13 5:31
Thomas Willwacher14-Sep-13 5:31 
QuestionFind dialog migrates each time it appears Pin
Chuck_Esterbrook12-Sep-13 21:40
Chuck_Esterbrook12-Sep-13 21:40 
AnswerRe: Find dialog migrates each time it appears Pin
Thomas Willwacher14-Sep-13 5:50
Thomas Willwacher14-Sep-13 5:50 
SuggestionMinor Update To Preserve BackgroundColor of matched text in RichTextBoxAdapter Pin
avaleri18-Apr-13 9:14
avaleri18-Apr-13 9:14 
Thanks for this, I found it very useful. I made 1 minor update to the RichTextBoxAdapter class to preserve the background color of the text so that if some text has an existing background color it does not get erased when the text is found in a search.

C#
object backgroundColor;

public void Select(int start, int length)
{
    TextPointer tp = rtb.Document.ContentStart;
    rtb.Selection.Select(GetPoint(tp, start), GetPoint(tp, start + length));
    rtb.ScrollToVerticalOffset(rtb.Selection.Start.GetCharacterRect(LogicalDirection.Forward).Top);
    backgroundColor = rtb.Selection.GetPropertyValue(TextElement.BackgroundProperty);
    rtb.Selection.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Yellow);
    oldsel = new TextRange(rtb.Selection.Start, rtb.Selection.End);
    rtb.SelectionChanged += rtb_SelectionChanged;
}

void rtb_SelectionChanged(object sender, RoutedEventArgs e)
{
    oldsel.ApplyPropertyValue(TextElement.BackgroundProperty, backgroundColor);
    rtb.SelectionChanged -= rtb_SelectionChanged;
}

GeneralRe: Minor Update To Preserve BackgroundColor of matched text in RichTextBoxAdapter Pin
Thomas Willwacher21-Apr-13 8:58
Thomas Willwacher21-Apr-13 8:58 
QuestionA bug with slow search in RichTextBox is fixed Pin
FenRunner27-Apr-12 4:48
FenRunner27-Apr-12 4:48 
AnswerRe: A bug with slow search in RichTextBox is fixed Pin
Thomas Willwacher27-Apr-12 23:11
Thomas Willwacher27-Apr-12 23:11 
BugBug when searching to the top Pin
SquidSK1-Mar-12 7:36
SquidSK1-Mar-12 7:36 
AnswerRe: Bug when searching to the top Pin
Thomas Willwacher2-Mar-12 5:27
Thomas Willwacher2-Mar-12 5:27 
QuestionEasy way to show a find-only dialog? Pin
Idel Lopez5-Jan-12 13:47
Idel Lopez5-Jan-12 13:47 
AnswerRe: Easy way to show a find-only dialog? Pin
Thomas Willwacher7-Jan-12 9:45
Thomas Willwacher7-Jan-12 9:45 
GeneralRe: Easy way to show a find-only dialog? Pin
Idel Lopez11-Apr-12 8:59
Idel Lopez11-Apr-12 8:59 
BugBug? in FindNext routine Pin
Ramiz_Zeynalov27-Dec-11 0:54
Ramiz_Zeynalov27-Dec-11 0:54 
GeneralRe: Bug? in FindNext routine Pin
Thomas Willwacher7-Jan-12 9:47
Thomas Willwacher7-Jan-12 9:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.