Infovark Underground

  • news
    • infoblog
    • underground
  • product
  • download
  • buy
  • support
  • about
    • ← Getting XAML Hyperlink text to wrap
    • Using Modal Dialogs with a Splash Screen in WPF →

    Highlighting query terms in a WPF TextBlock

    03 Mar 2011 by Dean in Programming, WPF / No Comments

    So you’d like to highlight the query terms used in a full text search within your WPF or Silverlight application. It’s a common use case in many web applications, but it’s becoming an increasingly common one in desktop applications as well.

    The effect you want to achieve is simple. Given that a user searched for the words highlighting and terms the following snippet of text:

    …we found that highlighting query terms made it much easier for users…

    Should be displayed like this:

    …we found that highlighting query terms made it much easier for users…

    A light touch for repeated use

    Your first instinct might be to use a FlowDocument displayed within a RichTextBox or FlowDocumentViewer. FlowDocuments have a convenient API for applying formatting to words or phrases, but they are heavyweight objects. Since we’ll display many search results on the same screen, the repeated use of FlowDocuments and viewer/editor controls will increase memory use and be a drag on performance. It’s not a good idea to use them within the ItemsControl or ListBox item templates that present search results.

    Fortunately, we can get term highlighting to work within a lightweight TextBlock control, if we use a few tricks. Our approach binds the text snippet to ContentControl using a converter that generates XAML objects.

    The overview

    First, let’s assume that your full-text search engine produces a string that contains delimiters to indicate where the highlights should begin and end. Lucene.Net, the content indexing engine we use for Infovark, has a Highlighter plugin that works this way.

    In Lucene.Net, the default delimiters used are opening and closing <B></B> tags. We’ll need to change these default settings to use delimiters that won’t cause trouble for XML parsing. (Remember, your text snippet could contain HTML or characters invalid in an XML document, so you’ll need to escape its output. We don’t want our delimiters to get wiped away during the escape process!)

    We decided to use the sequences |~S~| and |~E~| as these were highly unlikely to show up naturally in our snippets. Using our example from above, the highlighted snippet from Lucene.Net would look like this:

    …we found that |~S~|highlighting|~E~| query |~S~|terms|~E~| made it much easier for users…

    Now that we have our search result snippet formatted the way we want, we can build an IValueConverter that generates a TextBlock with internal Run elements. Here’s the full class.

    1.     /// <summary>
    2.     /// Converts a string containing valid XAML into WPF objects.
    3.     /// </summary>
    4.     [ValueConversion(typeof(string), typeof(object))]
    5.     public sealed class StringToXamlConverter : IValueConverter
    6.     {
    7.         /// <summary>
    8.         /// Converts a string containing valid XAML into WPF objects.
    9.         /// </summary>
    10.         /// <param name="value">The string to convert.</param>
    11.         /// <param name="targetType">This parameter is not used.</param>
    12.         /// <param name="parameter">This parameter is not used.</param>
    13.         /// <param name="culture">This parameter is not used.</param>
    14.         /// <returns>A WPF object.</returns>
    15.         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    16.         {
    17.             string input = value as string;
    18.             if(input != null)
    19.             {
    20.                 string escapedXml = SecurityElement.Escape(input);
    21.                 string withTags = escapedXml.Replace("|~S~|", "<Run Style=\"{DynamicResource highlight}\">");
    22.                 withTags = withTags.Replace("|~E~|", "</Run>");
    23.  
    24.                 string wrappedInput = string.Format("<TextBlock xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" TextWrapping=\"Wrap\">{0}</TextBlock>", withTags);
    25.  
    26.                 using (StringReader stringReader = new StringReader(wrappedInput))
    27.                 {
    28.                     using(XmlReader xmlReader = XmlReader.Create(stringReader))
    29.                     {
    30.                         return XamlReader.Load(xmlReader);
    31.                     }
    32.                 }
    33.             }
    34.  
    35.             return null;
    36.         }
    37.  
    38.         /// <summary>
    39.         /// Converts WPF framework objects into a XAML string.
    40.         /// </summary>
    41.         /// <param name="value">The WPF Famework object to convert.</param>
    42.         /// <param name="targetType">This parameter is not used.</param>
    43.         /// <param name="parameter">This parameter is not used.</param>
    44.         /// <param name="culture">This parameter is not used.</param>
    45.         /// <returns>A string containg XAML.</returns>
    46.         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    47.         {
    48.             throw new NotImplementedException("This converter cannot be used in two-way binding.");
    49.         }
    50.     }

    Now I’ll explain what we’re doing in the Convert() method in detail.

    Step by step

    First, we check to make sure that we actually have a string object with some text. Then we take that string and run it through the System.Security.SecurityElement.Escape() routine, a handy method in the .NET Framework for replacing invalid XML characters with properly escaped ones.

    1. string input = value as string;
    2. if(input != null)
    3. {
    4.     string escapedXml = SecurityElement.Escape(input);
    5.     …
    6. }

    Next, we replace the start delimiters with opening Run tags. Note that we set a style on this tag using a dynamic resource. This lets to apply whatever style we like to the highlighted words without having to mess with our converter logic. We also replace the end delimiters with a closing Run tag.

    1. string withTags = escapedXml.Replace("|~S~|", "<Run Style=\"{DynamicResource highlight}\">");
    2. withTags = withTags.Replace("|~E~|", "</Run>");

    Then we wrap the snippet inside a TextBlock. I’ve set the TextWrapping property to Wrap so that our line of text doesn’t run off the edge of the screen in our UI. Note that I’ve also included the XAML namespace; this will become important later when we use the XamlReader.

    1. string wrappedInput = string.Format("<TextBlock xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" TextWrapping=\"Wrap\">{0}</TextBlock>", withTags);

    Now we should have a well-formed XAML string. We feed this string into a StringReader, then an XmlReader, and finally a XamlReader. The XamlReader.Create() method instantiates the framework objects returned by our converter.

    1. using (StringReader stringReader = new StringReader(wrappedInput))
    2. {
    3.  using(XmlReader xmlReader = XmlReader.Create(stringReader))
    4.  {
    5.   return XamlReader.Load(xmlReader);
    6.  }
    7. }

    With our converter working properly, we can use it in the DataTemplates for our search results like this:

    1. <ContentControl Content="{Binding TextSnippet, Converter={StaticResource stringToXaml}, Mode=OneTime}"/>

    It’s quite a bit of work, but the end result is exactly what we want: a lightweight way of highlighting query terms in search results.

    If you’ve got comments or suggestions about this approach, we’d love to hear them.

    Related posts

    1. How to format the XAML Hyperlink NavigateUri
    2. Getting XAML Hyperlink text to wrap
    • Tweet
    • Tags:
    • highlighting
    • Lucene.Net
    • search
    • textblock
    • xaml

    Leave a Comment

    Posting your comment...

    Subscribe to these comments via email

    • Categories

      • .NET (41)
      • AJAX (3)
      • Books (7)
      • HTML (9)
      • Infovark (8)
      • Programming (48)
      • REST (11)
      • SQL (3)
      • Testing (3)
      • Tools (13)
      • UI (3)
      • WCF (11)
      • Web Services (8)
      • WPF (4)
      • XML (4)
    • Archives

    • Get future articles


       

    • Blogroll

      • Ajaxian
      • Anne Van Kesteren
      • Brain.Save()
      • Coding Horror
      • Eric Sink
      • Joel Spolsky
      • John Resig
      • Mark Pilgrim
      • Raymond Chen
      • Scott Hansleman
      • Secret Geek
      • Steve Yegge
      • The Daily WTF
      • The Database Programmer
    • Meta

      • Log in
      • Entries RSS
      • Comments RSS
      • WordPress.org
  • Site map

    • News
    • Product
    • Download
    • Buy
    • Support
    • About
  • Recent Posts

    • Review: Brownfield Application Development in .NET
    • Using Modal Dialogs with a Splash Screen in WPF
    • Highlighting query terms in a WPF TextBlock
    • Getting XAML Hyperlink text to wrap
    • How to format the XAML Hyperlink NavigateUri
  • Twitter

    Copyright 2011 Infovark, Inc. All rights reserved.