WPF Customized Find Control for FlowDocuments

There are a couple of controls in the WPF that can be used to load and display FlowDocuments. These controls have a Find feature that allows the users to find a word or a phrase in the FlowDocuments. Although, the find feature is very nice, but unfortunately, Microsoft defines it as an internal control and one can’t customize it. In this paper, I developed an out of the box Find control that is customizable and can be used anywhere.

Introduction

Last month, me and my colleague (Afshin Khashei) worked on a document management system that deals with lots of FlowDocuments. Flow documents are designed to optimize viewing and readability. Rather than being set to one predefined layout, flow documents dynamically adjust and reflow their content based on run-time variables such as window size, device resolution, and optional user preferences. FlowDocument Hosts and formats flow content with advanced document features, such as pagination and columns [MSDN]. The Blocking parts of a FlowDocument are instances of the Block class which has many derivations such as Figure, Floater, List, ListItem, Section and Table. Alongside the Block class there is another class called Inline. Each inline represents a piece of text or a special inline-level format for its child inlines. Run, Span, Bold, and Underline are Examples of Inline classes. Block level and Inline Level classes together provide a consistent way for generating custom flow documents. Here is a sample flowdocument.

<FlowDocument

Name="flowDoc"

TextAlignment="Justify"

IsOptimalParagraphEnabled="True"

IsHyphenationEnabled="True"

IsColumnWidthFlexible="True"

Background="AliceBlue"

ColumnWidth="300"

ColumnGap="20"

>

<Paragraph>

<Italic>

<Run>

"One of the most important operations necessary when text materials

are prepared for printing or display is the task of dividing long

paragraphs into individual lines. When this job has been done well,

people will not be aware of the fact that the words they are reading

have been broken apart arbitrarily and placed into a somewhat rigid

and unnatural rectangular framework; but if the job has been done

poorly, readers will be distracted by bad breaks that interrupt

their train of thought."

</Run></Italic>

</Paragraph>

<Paragraph>

<Hyperlink NavigateUri="http://www-cs-faculty.stanford.edu/~knuth/">

- Donald E. Knuth

</Hyperlink>

</Paragraph>

<Paragraph>

<Bold>

Principle of Optimal Paragraph

</Bold>

</Paragraph>

<Paragraph>

Knuth started his revolutionary work of developing a computer program to

produce high quality publication in the late ’70. The program he developed

called TEX (pronounced "tek") is highly regarded as the work that helps

shape the field of digital typography and desktop publishing. It is still

being widely used and constantly extended by many others up to today. One

of the most intriguing inventions in this program is its ability to produce

superior quality paragraph layout for printing and reading purpose – arguably

comparable to the work manually done by most respectable publishers of the

modern times. This typographically beautiful paragraph layout is driven by

a line breaking algorithm known as

<Italic>total-fit</Italic> or

<Italic>optimum-fit</Italic> algorithm. Some call the kind of paragraph

produced by this algorithm

<Italic>Optimal Paragraph</Italic> .

</Paragraph>

<Paragraph>

In principle, the task of text formatting consists of two main components:

choosing where to end individual lines and how to justify the lines. The first

component may involve the task of word division called

<Italic>hyphenation</Italic> .

Line justification is the task of fitting a line into a desired width. Usually

it is done by distributing the extra space into or taking out excess space

from inter-word spaces in the line. Line justification is strongly dependent

to line breaking. When line breaking is done properly, justification can be

done without the need to change the spacing too much and thereby avoiding

holes between words that would distress the eyes of the reader.

</Paragraph>

<Paragraph>

Unlike a standard line breaking algorithm which breaks the line without taking

into account the line that may come after it, the total-fit algorithm breaks

line by looking ahead on what may come later in the paragraph and make a single

decision to break all the lines at once. The main idea of the algorithm is to

provide a way to break a paragraph into lines so that the inter-word spacing is

balanced between all the lines of the paragraph. This is attained by choosing

the sequence of breakpoints with the minimal total cost over all lines. The

cost of a line depends on many factors that can affect the visual appearance

of the line, such as the measure of inter-word space changing, the division of

the last word of the line, etc.

</Paragraph>

<Paragraph>

A paragraph is considered optimal when all inter-word spaces over all lines are set as

close as possible to the ideal inter-word spaces. Therefore, an optimum paragraph is in

fact a paragraph composed in such a way that the total contrast of inter-word spaces set

in all lines with the ideal inter-word spaces cannot be reduced anymore.

</Paragraph>

</FlowDocument>

The above FlowDocument looks like as follow in run time.

WPF provides a couple of controls that can save/load flowdocuments including FlowDocumentReader, RichTextBox, FlowDocumentPageViewer and FlowDocumentScrollViewer. One of the shining features of those controls is their Find option that allows users to find a word or a phrase in a document. Find Control provides a nice user interface and using it is easy.

Last month, we faced a situation that we need to customize the UI and functionality of the build in Find control. After investigating the ControlTemplate of the FlowDocumentReader, we realized that the original Find Control can’t be customized, because Microsoft defines it as Internal and there is no way to access it! And shamefully, there is no news about it in the dotnet Framework 4 too. As a result, me and my colleague (Afshin Khashei) decided to implement a find control from the scratch. We expect the following functionalities from the new control.

· Expanding and collapsing the control similar to the original one.

· Highlighting the founded search term in the FlowDocument.

· Highlighting the Paragraph of the founded search term by a different color.

· The ability to navigate through founded patters.

· The ability to customize Font, Color, Background image and Background text of the control.

· The ability to put the control outside of the FlowDocumentReader.

The blue items don’t support by the current Find Control. We define our control as a custom UserControl that gets the FlowDocument via its Document property and starts the search operation whenever user expand the control and press the Enter after entering the search term. The control is flexible and using it is easy too.

The rest of the paper is organized as follows. In the first section, I will illustrate the process of finding a word inside a FlowDocument. In the second part, I will describe the process of how to highlight a word inside a FlowDocument. There is a light glance at the code of the Control in the third section and finally the last section is dedicated to using of the control.

Finding Text inside a FlowDocument

A FlowDocument can contain lots of different Blocks and Inlines, but, all of the texts inside a FlowDocument have been represented by the Run class. The other Blocks and Inlines are used for formatting and structuring the layout. An experienced reader who worked on FlowDocuments may say “No, you are wrong, I can put text inside other Inlines or Blocks like the following:

<Paragraph>

<Italic>

"One of the most important operations necessary when text materials

are prepared for printing or display is the task of dividing long

paragraphs into individual lines. When this job has been done well,

people will not be aware of the fact that the words they are reading

have been broken apart arbitrarily and placed into a somewhat rigid

and unnatural rectangular framework; but if the job has been done

poorly, readers will be distracted by bad breaks that interrupt

their train of thought."

</Italic>

</Paragraph>

But the reality is, as soon as the WPF loads the above paragraph, it changes the code as follow implicitly.

Paragraph>

<Italic>

<Run>

"One of the most important operations necessary when text materials

are prepared for printing or display is the task of dividing long

paragraphs into individual lines. When this job has been done well,

people will not be aware of the fact that the words they are reading

have been broken apart arbitrarily and placed into a somewhat rigid

and unnatural rectangular framework; but if the job has been done

poorly, readers will be distracted by bad breaks that interrupt

their train of thought."

</Run></Italic>

As a matter of fact, the WPF inserts the Run instances apparently whenever it is needed. So, in order to find a special text inside a FlowDocument, we should navigate through the Run instances and check their Text properties to find a matching with the search phrase. The following class contains the methods that can be used to return all Run instances inside a FlowDoucment.

public static class LogicalTreeUtility

{

public static IEnumerable GetChildren(DependencyObject obj, Boolean allChildrenInHierachy)

{

if (!allChildrenInHierachy)

return LogicalTreeHelper.GetChildren(obj);

else

{

List<object> ReturnValues = new List<object>();

RecursionReturnAllChildren(obj, ReturnValues);

return ReturnValues;

}

}

private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)

{

foreach (object curChild in LogicalTreeHelper.GetChildren(obj))

{

returnValues.Add(curChild);

if (curChild is DependencyObject)

RecursionReturnAllChildren((DependencyObject)curChild, returnValues);

}

}

public static IEnumerable<ReturnType> GetChildren<ReturnType>(DependencyObject obj, Boolean allChildrenInHierachy)

{

foreach (object child in GetChildren(obj, allChildrenInHierachy))

if (child is ReturnType)

yield return (ReturnType)child;

}

}

The above codes are based on a dotnet framework class called LogicalTreeHelper. This class has the GetChildren method that returns all of the direct childes of a given DependencyObject. In order to return all childes including grand childes and …, we have to call the GetChildren method recursively as follows:

private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)

{

foreach (object curChild in LogicalTreeHelper.GetChildren(obj))

{

returnValues.Add(curChild);

if (curChild is DependencyObject)

RecursionReturnAllChildren((DependencyObject)curChild, returnValues);

}

}

Using the LogicalTreeUtility.GetChildren method, one could get all of the Runs inside a document and check whether their Text properties contain the given search term or not. Seems to be easy, isn’t it?

But the story isn’t finished and finding the matched Runs is not sufficient. We need to highlight the founded words too!

TextRange, TextPointers and TextEffect

The TextPointer class is intended to facilitate traversal and manipulation of content that is represented by Windows Presentation Foundation (WPF) flow content elements. It provides a way to address the positions inside a FlowDocument. Some of the operations that TextPointer facilitates include the following:

· Perform an ordinal comparison of the current position with a second specified position.

· Determine the type of content adjacent to the current position in a specified direction.

· Get the TextElement that scopes or is adjacent to the current position.

· Get the text container that scopes the current document.

· Get a specified number of characters preceding or following the current position.

· Insert a string of characters at the current position.

· Find line boundaries in content.

· Translate between TextPointer positions and symbol offsets into content. See the GetOffsetToPosition and GetPositionAtOffset methods.

[MSDN]

On the other hand, a TextRange specifies a selection of content surrounded by two TextPointers. In order to address the search term inside a FlowDocument, we have to find a TextRange that its starting pointer points to the start of the word and its End pointer points to the end of the word in the document. t

After finding the TextRange, we have to highlight it. The highlighting process should be done without affecting the foregrounds and styles of the underlying document. Fortunately, text highlighting can be done transparently using the TextEffect class. The TextEffect object allows you to add effects such as animations to text objects such as TextBlock, TextElement, and FlowDocument objects. The following code assign a foreground color to a target that specified by a TextRange using a TextEffect.

public static TextEffect HighLightText(TextRange range, Brush brush)

{

TextEffect effect = new TextEffect();

effect.Foreground = brush;

if (range.Start.Parent is Inline)

{

Inline parent = range.Start.Parent as Inline;

effect.PositionStart = Math.Abs(range.Start.GetOffsetToPosition(parent.ContentStart));

effect.PositionCount = Math.Abs(range.End.GetOffsetToPosition(range.Start));

}

else if (range.Start.Parent is Block)

{

Block parent = range.Start.Parent as Block;

effect.PositionStart = Math.Abs(range.Start.GetOffsetToPosition(parent.ContentStart));

effect.PositionCount = Math.Abs(range.End.GetOffsetToPosition(range.Start));

}

TextEffectTarget[] targets = TextEffectResolver.Resolve(range.Start, range.End, effect);

foreach (TextEffectTarget target in targets)

{

target.Enable();

}

return effect;

}

In case user collapses the Find control, the TextEffects should be sweep off from the document. And the following code, clear all of the TextEffects inside a document.

public static void ClearTextDecorations(DependencyObject document)

{

// Traverse all run tags

foreach (Run run in LogicalTreeUtility.GetChildren<Run>(document, true))

{

run.TextEffects = new TextEffectCollection();

}

}

Find Control

And finally here is the code of the FindControl. Here is the code of FindControl itself.

<UserControl x:Name="findControl" x:Class="ICP.Controls.Document.FindControl"

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

xmlns:s="clr-namespace:System;assembly=mscorlib"

xmlns:flowDocumentControls="clr-namespace:ICP.Controls.Document"

xmlns:converters="clr-namespace:ICP.Controls.Converters"

xmlns:controls="clr-namespace:ICP.Controls"

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

<UserControl.Resources>

<converters:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"></converters:BooleanToVisibilityConverter>

</UserControl.Resources>

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"></ColumnDefinition>

<ColumnDefinition Width="*"></ColumnDefinition>

</Grid.ColumnDefinitions>

<ToggleButton Grid.Column="0" Click="FindButton_Click" Name="FindButton" Margin="3,0,3,0" ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=ToolTip}" x:Uid="ToggleButton_1" Focusable="True">

<ToggleButton.Style>

<Style TargetType="ToggleButton">

<Style.BasedOn>

<Style TargetType="ButtonBase">

<Style.Triggers>

<Trigger Property="UIElement.IsEnabled">

<Setter Property="UIElement.Opacity">

<Setter.Value>

<s:Double>0.3</s:Double>

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>False</s:Boolean>

</Trigger.Value>

</Trigger>

<Trigger Property="UIElement.IsMouseOver">

<Setter Property="UIElement.Opacity">

<Setter.Value>

<s:Double>1</s:Double>

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>True</s:Boolean>

</Trigger.Value>

</Trigger>

</Style.Triggers>

<Style.Resources>

<ResourceDictionary />

</Style.Resources>

<Setter Property="UIElement.Focusable">

<Setter.Value>

<s:Boolean>False</s:Boolean>

</Setter.Value>

</Setter>

<Setter Property="UIElement.Opacity">

<Setter.Value>

<s:Double>0.5</s:Double>

</Setter.Value>

</Setter>

<Setter Property="FrameworkElement.Cursor">

<Setter.Value>

<Cursor>Hand</Cursor>

</Setter.Value>

</Setter>

<Setter Property="Panel.Background">

<Setter.Value>

<SolidColorBrush>#00FFFFFF</SolidColorBrush>

</Setter.Value>

</Setter>

<Setter Property="Control.Padding">

<Setter.Value>

<Thickness>3,1,3,1</Thickness>

</Setter.Value>

</Setter>

<Setter Property="Border.BorderBrush">

<Setter.Value>

<x:Null />

</Setter.Value>

</Setter>

<Setter Property="Border.BorderThickness">

<Setter.Value>

<Thickness>0,0,0,0</Thickness>

</Setter.Value>

</Setter>

<Setter Property="FrameworkElement.MinWidth">

<Setter.Value>

<s:Double>0</s:Double>

</Setter.Value>

</Setter>

<Setter Property="FrameworkElement.MinHeight">

<Setter.Value>

<s:Double>0</s:Double>

</Setter.Value>

</Setter>

<Setter Property="Control.Template">

<Setter.Value>

<ControlTemplate TargetType="ButtonBase">

<Border Background="{TemplateBinding Panel.Background}" x:Uid="Border_39">

<ContentPresenter Content="{TemplateBinding ContentControl.Content}" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}" Name="Content" RenderTransformOrigin="0.5,0.5" x:Uid="ContentPresenter_2" />

</Border>

<ControlTemplate.Triggers>

<Trigger Property="UIElement.IsMouseOver">

<Setter Property="UIElement.RenderTransform" TargetName="Content">

<Setter.Value>

<ScaleTransform ScaleX="1.1" ScaleY="1.1" />

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>True</s:Boolean>

</Trigger.Value>

</Trigger>

<Trigger Property="ButtonBase.IsPressed">

<Setter Property="UIElement.RenderTransform" TargetName="Content">

<Setter.Value>

<ScaleTransform ScaleX="0.9" ScaleY="0.9" />

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>True</s:Boolean>

</Trigger.Value>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</Style.BasedOn>

<Style.Triggers>

<Trigger Property="UIElement.IsEnabled">

<Setter Property="UIElement.Visibility">

<Setter.Value>

<x:Static Member="Visibility.Collapsed" />

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>False</s:Boolean>

</Trigger.Value>

</Trigger>

<Trigger Property="ToggleButton.IsChecked">

<Setter Property="UIElement.Opacity">

<Setter.Value>

<s:Double>1</s:Double>

</Setter.Value>

</Setter>

<Trigger.Value>

<s:Boolean>True</s:Boolean>

</Trigger.Value>

</Trigger>

</Style.Triggers>

<Style.Resources>

<ResourceDictionary />

</Style.Resources>

</Style>

</ToggleButton.Style>

<Path Stroke="Black" VerticalAlignment="Center" x:Uid="Path_23">

<Path.Data>

<GeometryGroup>

<GeometryGroup.Children>

<RectangleGeometry RadiusX="1" RadiusY="1" Rect="0.5,0.5,19,19" />

<EllipseGeometry RadiusX="5" RadiusY="5" Center="12,8" />

<EllipseGeometry RadiusX="4" RadiusY="4" Center="12,8" />

<LineGeometry StartPoint="2.5,16.5" EndPoint="9,10" />

<LineGeometry StartPoint="3,17" EndPoint="9.5,10.5" />

<LineGeometry StartPoint="3.5,17.5" EndPoint="10,11" />

</GeometryGroup.Children>

</GeometryGroup>

</Path.Data>

</Path>

</ToggleButton>

<Grid Grid.Column="1" x:Name="findTextBoxGrid" Background="{Binding ElementName=findControl, Path=TextBoxBackgroundBrush}">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="10*"></ColumnDefinition>

<ColumnDefinition Width="Auto"></ColumnDefinition>

<ColumnDefinition Width="Auto"></ColumnDefinition>

</Grid.ColumnDefinitions>

<Label

FontFamily="{Binding ElementName=findControl, Path=FontFamily}"

FontSize="{Binding ElementName=findControl, Path=FontSize}"

FontStyle="{Binding ElementName=findControl, Path=FontStyle}"

FontWeight="{Binding ElementName=findControl, Path=FontWeight}"

FontStretch="{Binding ElementName=findControl, Path=FontStretch}"

Name="backgroundLabel"

Grid.Column="0"

Foreground="{Binding ElementName=findControl, Path=BackgroundTextBrush}"

Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=BackgroundText}"

Visibility="{Binding ElementName=findTextBox, Path=Text.IsEmpty, Converter={StaticResource booleanToVisibilityConverter}}"

HorizontalAlignment="Left" VerticalAlignment="Center"/>

<TextBox Background="Transparent" Grid.Column="0"

Padding="0"

FontFamily="{Binding ElementName=findControl, Path=FontFamily}"

FontSize="{Binding ElementName=findControl, Path=FontSize}"

FontStyle="{Binding ElementName=findControl, Path=FontStyle}"

FontWeight="{Binding ElementName=findControl, Path=FontWeight}"

FontStretch="{Binding ElementName=findControl, Path=FontStretch}"

BorderThickness="0"

Name="findTextBox"

KeyDown="findTextBox_KeyDown"

HorizontalAlignment="Left"

VerticalAlignment="Center"

/>

<controls:ContentButton Grid.Column="1" Width="15px" x:Name="previousButton" Click="previousButton_Click">

<Path VerticalAlignment="Center" HorizontalAlignment="Center" Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextPreviousButtonsForegroundBrush}" x:Uid="Path_18">

<Path.Data>

<PathGeometry Figures="M5,0L5,10L0,5z" />

</Path.Data>

</Path>

<controls:ContentButton.ToolTip>

<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=PreviousButtonHintText}"></TextBlock>

</controls:ContentButton.ToolTip>

</controls:ContentButton>

<controls:ContentButton Grid.Column="2" Width="15px" x:Name="nextButton" Click="nextButton_Click">

<Path VerticalAlignment="Center" HorizontalAlignment="Center" Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextPreviousButtonsForegroundBrush}" x:Uid="Path_19">

<Path.Data>

<PathGeometry Figures="M0,0L0,10L5,5z" />

</Path.Data>

</Path>

<controls:ContentButton.ToolTip>

<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextButtonHintText}"></TextBlock>

</controls:ContentButton.ToolTip>

</controls:ContentButton>

</Grid>

</Grid>

</UserControl>

public partial class FindControl : UserControl, INotifyPropertyChanged

{

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected void notifyPropertyChanged(string name)

{

if (PropertyChanged != null)

{

PropertyChanged(this, new PropertyChangedEventArgs(name));

}

}

#endregion

public FindControl()

{

InitializeComponent();

setFindTextBoxVisibilty();

}

#region Properties

public string SearchText

{

get

{

return (string)this.GetValue(SearchTextProperty);

}

set

{

this.SetValue(SearchTextProperty, value);

}

}

public static readonly DependencyProperty SearchTextProperty = DependencyProperty.Register(

"SearchText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());

public FlowDocument Document

{

get

{

return (FlowDocument)this.GetValue(DocumentProperty);

}

set

{

this.SetValue(DocumentProperty, value);

}

}

public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register(

"Document", typeof(FlowDocument), typeof(FindControl), new FrameworkPropertyMetadata());

public Brush TextBoxBackgroundBrush

{

get

{

return (Brush)this.GetValue(TextBoxBackgroundBrushProperty);

}

set

{

this.SetValue(TextBoxBackgroundBrushProperty, value);

}

}

public static readonly DependencyProperty TextBoxBackgroundBrushProperty = DependencyProperty.Register(

"TextBoxBackgroundBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());

public Brush BackgroundTextBrush

{

get

{

return (Brush)this.GetValue(BackgroundTextBrushProperty);

}

set

{

this.SetValue(BackgroundTextBrushProperty, value);

}

}

public static readonly DependencyProperty BackgroundTextBrushProperty = DependencyProperty.Register(

"BackgroundTextBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());

public string BackgroundText

{

get

{

return (string)this.GetValue(BackgroundTextProperty);

}

set

{

this.SetValue(BackgroundTextProperty, value);

}

}

public static readonly DependencyProperty BackgroundTextProperty = DependencyProperty.Register(

"BackgroundText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());

public string NextButtonHintText

{

get

{

return (string)this.GetValue(NextButtonHintTextProperty);

}

set

{

this.SetValue(NextButtonHintTextProperty, value);

}

}

public static readonly DependencyProperty NextButtonHintTextProperty = DependencyProperty.Register(

"NextButtonHintText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());

public string PreviousButtonHintText

{

get

{

return (string)this.GetValue(PreviousButtonHintTextProperty);

}

set

{

this.SetValue(PreviousButtonHintTextProperty, value);

}

}

public static readonly DependencyProperty PreviousButtonHintTextProperty = DependencyProperty.Register(

"PreviousButtonHintText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());

public Brush HighlightedTextBrush

{

get

{

return (Brush)this.GetValue(HighlightedTextBrushProperty);

}

set

{

this.SetValue(HighlightedTextBrushProperty, value);

}

}

public static readonly DependencyProperty HighlightedTextBrushProperty = DependencyProperty.Register(

"HighlightedTextBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());

public Brush HighlightedParagraphBrush

{

get

{

return (Brush)this.GetValue(HighlightedParagraphBrushProperty);

}

set

{

this.SetValue(HighlightedParagraphBrushProperty, value);

}

}

public static readonly DependencyProperty HighlightedParagraphBrushProperty = DependencyProperty.Register(

"HighlightedParagraphBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());

public Brush NextPreviousButtonsForegroundBrush

{

get

{

return (Brush)this.GetValue(NextPreviousButtonsForegroundBrushProperty);

}

set

{

this.SetValue(NextPreviousButtonsForegroundBrushProperty, value);

}

}

public static readonly DependencyProperty NextPreviousButtonsForegroundBrushProperty = DependencyProperty.Register(

"NextPreviousButtonsForegroundBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());

private int currentFindedPatternPosition;

private List<TextRange> findedPatterns;

private DependencyObject highlightedParagraph;

private string filterText = "";

public Action RefreshHighLightedWordsHandler

{

get

{

return (Action)this.GetValue(RefreshHighLightedWordsHandlerProperty);

}

set

{

this.SetValue(RefreshHighLightedWordsHandlerProperty, value);

}

}

public static readonly DependencyProperty RefreshHighLightedWordsHandlerProperty = DependencyProperty.Register(

"RefreshHighLightedWordsHandler", typeof(Action), typeof(FindControl), new FrameworkPropertyMetadata());

#endregion

#region Methods

protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)

{

base.OnPropertyChanged(e);

if (e.Property.Name == "SearchText")

{

if (SearchText != null && SearchText.Length > 0)

{

FindButton.IsChecked = true;

setFindTextBoxVisibilty();

findTextBox.Text = SearchText;

DoSearch();

}

}

}

private void refreshHighlightedwordsSearch()

{

if (FindButton.IsChecked.HasValue && FindButton.IsChecked.Value && findedPatterns != null)

{

foreach (TextRange range in findedPatterns)

{

FlowDocumentExtension.HighLightText(range, HighlightedTextBrush);

}

}

}

private void setFindTextBoxVisibilty()

{

if (FindButton.IsChecked.HasValue && FindButton.IsChecked.Value)

{

findTextBoxGrid.Visibility = Visibility.Visible;

findTextBox.Text = "";

filterText = "";

currentFindedPatternPosition = -1;

findedPatterns = null;

findTextBox.Focus();

CreateButtonToolTips();

RefreshHighLightedWordsHandler = new Action(refreshHighlightedwordsSearch);

notifyPropertyChanged("RefreshSearchHandler");

}

else

{

findTextBoxGrid.Visibility = Visibility.Collapsed;

SearchText = "";

if (Document != null)

{

SetBackGround(highlightedParagraph, null);

FlowDocumentExtension.ClearTextDecorations(Document);

}

}

}

private void CreateButtonToolTips()

{

if (NextButtonHintText != null && NextButtonHintText.Length > 0)

{

ToolTip tooltip = new ToolTip();

TextBlock textBlock = new TextBlock();

textBlock.Text = NextButtonHintText;

tooltip.Content = textBlock;

nextButton.ToolTip = tooltip;

}

if (PreviousButtonHintText != null && PreviousButtonHintText.Length > 0)

{

ToolTip tooltip = new ToolTip();

TextBlock textBlock = new TextBlock();

textBlock.Text = PreviousButtonHintText;

tooltip.Content = textBlock;

previousButton.ToolTip = tooltip;

}

}

private void SetBackGround(DependencyObject target, Brush brush)

{

if (target != null)

{

if (target is Paragraph)

{

((Paragraph)target).Background = brush;

}

else if (target is TextElement)

{

((TextElement)target).Background = brush;

}

}

}

private void BringIntoViewNextPattern()

{

currentFindedPatternPosition++;

if (findedPatterns == null || findedPatterns.Count == 0)

{

return;

}

if (findedPatterns.Count <= currentFindedPatternPosition)

{

currentFindedPatternPosition = findedPatterns.Count - 1;

}

if (highlightedParagraph != null)

{

SetBackGround(highlightedParagraph, null);

}

highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Paragraph;

if (highlightedParagraph == null)

{

highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Parent;

}

LogicalTreeHelper.BringIntoView(highlightedParagraph);

SetBackGround(highlightedParagraph, HighlightedParagraphBrush);

}

private void BringIntoViewPreviousPattern()

{

currentFindedPatternPosition--;

if (findedPatterns == null || currentFindedPatternPosition < 0)

{

currentFindedPatternPosition = 0;

return;

}

if (highlightedParagraph != null)

{

SetBackGround(highlightedParagraph, null);

}

highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Paragraph;

if (highlightedParagraph == null)

{

highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Parent;

}

LogicalTreeHelper.BringIntoView(highlightedParagraph);

SetBackGround(highlightedParagraph, HighlightedParagraphBrush);

}

private void DoSearch()

{

if (Document == null)

{

return;

}

if ((filterText == findTextBox.Text && findedPatterns != null))

{

BringIntoViewNextPattern();

return;

}

if (filterText != findTextBox.Text)

{

FlowDocumentExtension.ClearTextDecorations(Document);

}

filterText = findTextBox.Text;

findedPatterns = FlowDocumentExtension.GetAllMatchingInParagraph(Document, filterText, false);

foreach (TextRange range in findedPatterns)

{

FlowDocumentExtension.HighLightText(range, HighlightedTextBrush);

}

currentFindedPatternPosition = -1;

BringIntoViewNextPattern();

}

#endregion

#region Events

private void findTextBox_KeyDown(object sender, KeyEventArgs e)

{

if (e.Key == Key.Enter)

{

DoSearch();

}

else if (e.Key == Key.Escape)

{

FindButton.IsChecked = false;

setFindTextBoxVisibilty();

}

}

private void FindButton_Click(object sender, RoutedEventArgs e)

{

setFindTextBoxVisibilty();

}

private void nextButton_Click(object sender, RoutedEventArgs e)

{

if (filterText != findTextBox.Text)

{

DoSearch();

}

else

{

BringIntoViewNextPattern();

}

}

private void previousButton_Click(object sender, RoutedEventArgs e)

{

if (filterText != findTextBox.Text)

{

DoSearch();

}

else

{

BringIntoViewPreviousPattern();

}

}

#endregion

}

Using the Code

Using the code is quite easy. The only thing that needs to be done is setting the properties of the Find control including the Document as follows:

<icp:FindControl

HorizontalAlignment="Left"

BorderThickness="1"

HighlightedTextBrush="Yellow"

HighlightedParagraphBrush="Aqua"

NextPreviousButtonsForegroundBrush="Blue"

Grid.Row="0"

Width="150"

Height="25"

BackgroundText="Form Search"

TextBoxBackgroundBrush="White"

BackgroundTextBrush="Gray"

Document="{Binding ElementName=docReader, Path=Document}"></icp:FindControl>

You can download the code here. At the end, I like to appreciate the efforts and design guidances of my colleague, Afshin Khashei, that help me to implement the control.

By Siyamand Ayubi   Popularity  (5942 Views)