Geeks With Blogs
New Things I Learned

One project was put forth that essentially requires creating a MessageBox look-alike, but the text supports rich-format, and we can do it in WPF.  It's a pretty neat task, and in this post I'll focus on just the text side; the easiest way possible most probably is to just use a RichTextBox control, make it to have no border, and everything will be fine.  However, I'd like to do it efficiently as well.

FormattedText can be used; however it's hard to make it generic - essentially code has to be written to show the text in the desired font/color.  The TextBlock class supports using some flow content elements, and it is described as very lightweight; so let's use that.  The sample provided shows the fact that it can display rich-formatted text:

XAML for the Textblock (from MSDN sample):

What the Textblock looks like (from MSDN sample):

This is fine when what you want to display resides in its own XAML; how about making it generic, where the consumer can specify the text to display, and also control the format/color/font size of the text - preferably also making the string to be available from resources?  Looking around, RichTextBox has a sample on how to get its content and the decorations.  Using the same methodology it works, but the returned string looks rather convoluted:

 

Code: 

private string GetTextBlockXAMLContent(TextBlock textBlock)
{
   // Get the content being displayed
   TextRange range = new TextRange(textBlock.ContentStart, textBlock.ContentEnd);
 
   // Save the content to stream and get it back as byte array
   MemoryStream stream = new MemoryStream();
   range.Save(stream, DataFormats.Xaml);
   byte[] bytes = stream.ToArray();
 
   // Convert to character and rebuild it into a string
   StringBuilder stringBuilder = new StringBuilder(bytes.Length);
   List<char> chars = new List<char>(bytes.Select(b => Convert.ToChar(b)));
   chars.ForEach(ch => stringBuilder.Append(ch));
 
   return stringBuilder.ToString();

}

Resulting string:
<Section xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml:space="preserve" TextAlignment="Left" LineHeight="Auto" IsHyphenationEnabled="False" xml:lang="en-us" FlowDirection="LeftToRight" NumberSubstitution.CultureSource="User" NumberSubstitution.Substitution="AsCulture" FontFamily="Tahoma" FontStyle="Normal" FontWeight="Normal" FontStretch="Normal" FontSize="11" Foreground="#FF000000" Typography.StandardLigatures="True" Typography.ContextualLigatures="True" Typography.DiscretionaryLigatures="False" Typography.HistoricalLigatures="False" Typography.AnnotationAlternates="0" Typography.ContextualAlternates="True" Typography.HistoricalForms="False" Typography.Kerning="True" Typography.CapitalSpacing="False" Typography.CaseSensitiveForms="False" Typography.StylisticSet1="False" Typography.StylisticSet2="False" Typography.StylisticSet3="False" Typography.StylisticSet4="False" Typography.StylisticSet5="False" Typography.StylisticSet6="False" Typography.StylisticSet7="False" Typography.StylisticSet8="False" Typography.StylisticSet9="False" Typography.StylisticSet10="False" Typography.StylisticSet11="False" Typography.StylisticSet12="False" Typography.StylisticSet13="False" Typography.StylisticSet14="False" Typography.StylisticSet15="False" Typography.StylisticSet16="False" Typography.StylisticSet17="False" Typography.StylisticSet18="False" Typography.StylisticSet19="False" Typography.StylisticSet20="False" Typography.Fraction="Normal" Typography.SlashedZero="False" Typography.MathematicalGreek="False" Typography.EastAsianExpertForms="False" Typography.Variants="Normal" Typography.Capitals="Normal" Typography.NumeralStyle="Normal" Typography.NumeralAlignment="Normal" Typography.EastAsianWidths="Normal" Typography.EastAsianLanguage="Normal" Typography.StandardSwashes="0" Typography.ContextualSwashes="0" Typography.StylisticAlternates="0" HasTrailingParagraphBreakOnPaste="False">
 
 <Span NumberSubstitution.CultureSource="Text" FontWeight="Bold">
 <Run>TextBlock</Run>
 </Span>
 <Run NumberSubstitution.CultureSource="Text">is designed to be</Run>
 <Span NumberSubstitution.CultureSource="Text" FontStyle="Italic">
 <Run>lightweight</Run>
 </Span>
 <Run NumberSubstitution.CultureSource="Text">, and is geared specifically at integrating</Run>
 <Span NumberSubstitution.CultureSource="Text" FontStyle="Italic">
 <Run>small</Run>
 </Span>
 <Run NumberSubstitution.CultureSource="Text">portions of flow content into a UI.</Run>
 </Section>

Essentially all the properties are set out; it makes things very hard to read - this can be a problem when we want to store the string in the resource, since translators only need to translate the literal parts.  The text size grows as well, causing quite a bit of size addition.  Is there a better shorthand?  I found one by using the XamlWriter / XamlReader constructs.

 

private object GetTextBlockXAMLContent(TextBlock textBlock)
{
   // Get the content being displayed
   string xaml = XamlWriter.Save(textBlock.Inlines);
   object o = XamlReader.Parse(xaml);
 
   return o;
}

However, when executing the code above, the XamlReader.Parse method throws a XamlParseException: Cannot create object of type 'System.Windows.Documents.InlineCollection'. CreateInstance failed, which can be caused by not having a public default constructor for 'System.Windows.Documents.InlineCollection'.  Error at Line 1 Position 2.  Apparently InlineCollection doesn't have a public default constructor.  Incidentally, the XamlReader.Parse method is new to .NET 3.5 SP1 (it is also added to .NET 3.0 SP2).  So we have to change the code so it enumerates through all the inlines as follows:

 

Code:

private string GetTextBlockXAMLContent(TextBlock textBlock)
{
   // Get the first inline
   Inline content = textBlock.Inlines.FirstInline;
 
   // If there are more than 1, then create a Span that
   // contains everything else
   if (textBlock.Inlines.Count > 1)
   {
      content = new Span();
      ((Span)content).Inlines.AddRange(textBlock.Inlines);
   }
 
   // Get the XAML that represents the TextBlock content
   string xaml = content == null ? null : XamlWriter.Save(content);
   return xaml;
}

XAML of the content:

<Span xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 <Bold>TextBlock</Bold>
 is designed to be
 <Italic>lightweight</Italic>
 , and is geared specifically at integrating
 <Italic>small</Italic>
 portions of flow content into a UI.
 </Span>

Code to set TextBlock Content:

private void SetTextBlockXAMLContent(TextBlock textBlock, string xaml)
{
   // Clear textblock content
   textBlock.Inlines.Clear();
   if (xaml == null)
      return;
 
   // Add xaml content
   textBlock.Inlines.Add(XamlReader.Parse(xaml) as Inline);
}

As you can see, the XAML content is fairly compact, makes it easier to translate & store. Based on the example above since the TextBlock has more than 1 element in it, we're just encapsulating it inside a Span element, and that element is the one that is then serialized as XAML.  The last piece is code to restore the XAML content to a TextBlock.  Some things to note: the method is fairly slow, if you make a sample to get a content of a TextBlock and assign that content to another TextBlock, the first time it runs you can see that the operation is not instantaneous.  TextBlock also accepts only Inlines; and won't be able to fully display a complex FlowDocument.  I hope this can be of some use to others.

Posted on Monday, November 17, 2008 5:18 PM WPF , .NET , Windows | Back to top


Comments on this post: Displaying Rich-Formatted Text in WPF

# re: Displaying Rich-Formatted Text in WPF
Requesting Gravatar...
Amazing! I've slightly modified this approach to be able bind formatted text to TextBlock. It works like a charm.
{code}
/// <summary>
/// Using this extension one could apply formatted text to TextBlock. Format is the same as allowed in plain xaml.
/// E.g. "I'm <Bold>very</Bold> happy" to highlight "very" in bold
/// </summary>
public static class FormattedText
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
"Value",
typeof(string),
typeof(FormattedText),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, PropertyChangedCallback));

private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
SetValue((TextBlock)dependencyObject, (string)dependencyPropertyChangedEventArgs.NewValue);
}

public static void SetValue(TextBlock textBlock, string value)
{
textBlock.SetValue(ValueProperty, value);

// clear text block
textBlock.Inlines.Clear();

if (string.IsNullOrEmpty(value))
return;

var xaml = "<Span xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" + value + "</Span>";

// Add xaml content
var inline = XamlReader.Parse(xaml) as Inline;
if (inline != null)
textBlock.Inlines.Add(inline);
}

public static string GetValue(DependencyObject element)
{
return (string)element.GetValue(ValueProperty);
}
}
{code}
Left by Mikhail on Apr 16, 2014 2:30 AM

Your comment:
 (will show your gravatar)


Copyright © Muljadi Budiman | Powered by: GeeksWithBlogs.net