I have TextBlock that has Inlines dynamicly added to it (basically bunch of Run objects that are either italic or bold).
In my application I have search function.
I took dthrasers answer and took out the need for an XML parser. He does a great job explaining each of the pieces in his blog, However this didn't require me to add any extra libraries, here's how I did it.
Step one, make a converter class:
class StringToXamlConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string input = value as string;
if (input != null)
{
var textBlock = new TextBlock();
textBlock.TextWrapping = TextWrapping.Wrap;
string escapedXml = SecurityElement.Escape(input);
while (escapedXml.IndexOf("|~S~|") != -1) {
//up to |~S~| is normal
textBlock.Inlines.Add(new Run(escapedXml.Substring(0, escapedXml.IndexOf("|~S~|"))));
//between |~S~| and |~E~| is highlighted
textBlock.Inlines.Add(new Run(escapedXml.Substring(escapedXml.IndexOf("|~S~|") + 5,
escapedXml.IndexOf("|~E~|") - (escapedXml.IndexOf("|~S~|") + 5)))
{ FontWeight = FontWeights.Bold, Background= Brushes.Yellow });
//the rest of the string (after the |~E~|)
escapedXml = escapedXml.Substring(escapedXml.IndexOf("|~E~|") + 5);
}
if (escapedXml.Length > 0)
{
textBlock.Inlines.Add(new Run(escapedXml));
}
return textBlock;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException("This converter cannot be used in two-way binding.");
}
}
Step two: Instead of a TextBlock use a ContentBlock. Pass in the string (you would of used for your textBlock) to the content block, like so:
<ContentControl Margin="7,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding Description, Converter={StaticResource CONVERTERS_StringToXaml}, Mode=OneTime}">
</ContentControl>
Step three:
Make sure the text you pass includes |~S~|
before and |~E~|
after the text part you want to be highlighted. For example in this string "my text |~S~|is|~E~| good"
the is
will be highlighted in yellow.
Notes:
You can change the style in the run to determine what and how your text is highlighted
Make sure you add your Converter class to your namespace and resources. This might also require a rebuild to get working.
This question is similar to How to display search results in a WPF items control with highlighted query terms
In answer to that question, I came up with an approach that uses an IValueConverter. The converter takes a text snippet, formats it into valid XAML markup, and uses a XamlReader to instantiate the markup into framework objects.
The full explanation is rather long, so I've posted it to my blog: Highlighting Query Terms in a WPF TextBlock
I had a similar problem - trying to implement a text search over a load of presenters that basically represent a report. The report was originally written into a string and we were leveraging FlowDocumentViewer's built in ctrl-F - it's not very good and has some wierd options but was sufficient.
If you just want something like that you can do the following:
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph FontFamily="Lucida Console" FontSize="12">
<Run Text="{Binding Content, Mode=OneWay}"/>
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
We decided to go for a rewrite as the report is kept in sync with the rest of the program and basically every edit changes it, having to recreate the entire report everytime means that this is quite slow. We wanted to improve this by moving to a update-the-bits-you-need-to model but needed to have view model (rather than just a string) to be able to do that in a sane way! We wanted to preserve the searching functionality before swapping out the report however and go one better and have highlighting of the 'current' search position in one colour and other search hits in another.
Here's a simplified version of my solution; a class that derives from TextBlock that adds a dependency property of Type HighlightingInformation. I've not included the namespace and usings as they are sensitive.
public class HighlightingTextBlock : TextBlock
{
public static readonly DependencyProperty HighlightingProperty =
DependencyProperty.Register("Highlighting", typeof (HighlightingInformation), typeof (HighlightingTextBlock));
public HighlightingInformation Highlighting
{
get { return (HighlightingInformation)GetValue(HighlightingProperty); }
set { SetValue(HighlightingProperty, value); }
}
public HighlightingTextBlock()
{
AddValueChangedCallBackTo(HighlightingProperty, UpdateText);
}
private void AddValueChangedCallBackTo(DependencyProperty property, Action updateAction)
{
var descriptor = DescriptorFor(property);
descriptor.AddValueChanged(this, (src, args) => updateAction());
}
private DependencyPropertyDescriptor DescriptorFor(DependencyProperty property)
{
return DependencyPropertyDescriptor.FromProperty(property, GetType());
}
private void UpdateText()
{
var highlighting = Highlighting;
if (highlighting == null)
return;
highlighting.SetUpdateMethod(UpdateText);
var runs = highlighting.Runs;
Inlines.Clear();
Inlines.AddRange(runs);
}
}
The type this class can be bound to uses the update method when it's text and list of highlights are changed to update the list of Runs. The highlights themselves look something like this:
public class Highlight
{
private readonly int _length;
private readonly Brush _colour;
public int Start { get; private set; }
public Highlight(int start, int length,Brush colour)
{
Start = start;
_length = length;
_colour = colour;
}
private string TextFrom(string currentText)
{
return currentText.Substring(Start, _length);
}
public Run RunFrom(string currentText)
{
return new Run(TextFrom(currentText)){Background = _colour};
}
}
To produce the correct collection of highlights is a seperate problem, which I basically solved by treating the collection of presenters as a Tree that you recursively search for content - leaf nodes are those that have content and other nodes just have children. If you search depth-first you get the order you'd expect. You can then basically write a wrapper around the list of results to keep track of the position. Im not going to post all the code for this - my response here it is to document how you can make wpf do multi-coloured highlighting in MVP style.
I haven't used INotifyPropertyChanged or CollectionChanged here as we didn't need the changes to be multi-cast (eg one presenter has multiple views). Initially I tried to do that by adding an event changed notification for Text and one for a list (which you also have to manually subscribe to the INotifyCollectionChanged event on). I had concerns about memory leaks from the event subcriptions however and the fact that the updates for the text and the highlights didn't come at the same time made it problematic.
The one drawback of this approach is that people shouldn't bind to the Text property of this control. In the real version I have added some checking + exception throwing to stop people from doing this but ommitted it from the example for clarity's sake!