This article -proudly- presents a WPF control that displays rich text from an HTML source: it has a Text dependency property that you can bind to a HTML-formatted string. It's designed to display -not edit- rich information messages like warnings, error messages, and other feedback e.g. from a localizable resource file. Here are a couple of examples, on the left side is the original HTML, on the right side is how it's displayed by the control:
Formatting:
Font sizes:
Colors:
Some issues with WPF's native RichTextBox control.
The HtmlTextBlock control is based on WPF's native RichTextBox control. This was not an obvious choice, since RichTextBox was actually designed as an editor control: by typing in the box and pressing toolbar buttons, a FlowDocument is created and displayed. This FlowDocument is accessible through the Document property, which unfortunately is
- very complex to manipulate, compared to a formatted HTML or XAML string (you can't store it in a resource file), and
- not a dependency property.
On the other hand, the FlowDocument has powerful text formatting features, so it really makes sense to try to leverage these.
Making a bindable RichTextBox
If you grab your favorite search engine and type "WPF Bindable RichTextBox" you'll find plenty of implementations. This one on CodePlex nicely explains the problems ànd solutions. Basically all you need to do is write a dependency property that is wrapped around the RichtTextBox' FlowDocument whilst converting it to and from a XAML-formatted string. Here's how this process looks like in code:
Dependency Property
These code snippets define the dependency property:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof(string),
typeof(RichTextBox),
new FrameworkPropertyMetadata(
String.Empty,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnTextPropertyChanged),
new CoerceValueCallback(CoerceTextProperty),
true,
System.Windows.Data.UpdateSourceTrigger.LostFocus));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
Conversion from XAML to FlowDocument
This method takes a XAML-formatted string and loads it into a FlowDocument:
public void SetText(System.Windows.Documents.FlowDocument document, string text)
{
try
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream(Encoding.ASCII.GetBytes(text)))
{
tr.Load(ms, DataFormats.Xaml);
}
}
catch
{
throw new InvalidDataException("data provided is not in the correct Xaml format.");
}
}
Conversion from FlowDocument to XAML
This function does the opposite. It saves a FlowDocument as a XAML-formatted string:
public string GetText(System.Windows.Documents.FlowDocument document)
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream())
{
tr.Save(ms, DataFormats.Xaml);
return ASCIIEncoding.Default.GetString(ms.ToArray());
}
}
The above code snippets originate from this RichtTextBox implementation from CodePlex (more about that later).
From TextBox to TextBlock
Since my control is a display panel instead of an edit box, I had to tweak the code in several places. Some extra properties were set in the constructor:
public RichTextBox()
{
Loaded += RichTextBox_Loaded;
//Added
this.IsReadOnly = true;
this.Focusable = false;
}
The original CodePlex control was not 100% percent bindable to a source: it was just Initializable from a source. After the source provided the initial value, all other updates were ignored. That was not compatible with my requirements: I wanted to bind the control to a dynamic value, like changing status information or an on-the-fly-localizable message. So I removed the code that inhibited long-term binding: the definition and all references and manipulation of the _textHasLoaded variable. Now the control does more, with less code.
All you need to do is bind the textbox to its source:
<uc:RichTextBox
Text="{Binding Message}"
</uc:RichTextBox>
And on every update of the source, the displayed text will also be updated:
this.Message = "The report was sent to <b>your local printer</b>.<br/><br/>Press <u style='color:red'>Enter</u> to continue or <u style='color:red'>Back</u> to print again.";
Here's the result:
Converting HTML to XAML, and back
The native WPF RichTextBox nicely displays plain text, RTF, and XAML, but no HTML. Fortunately a lot of attempts were already made to convert HTML into XAML and back. The code I'm currently using comes from MSDN. Its probably not the world's best converter, but it simply does what I was looking for: formatting (bold, italic, underline, size, color, and line breaks). It's not the best solution if you want to properly display tables, lists, headers and so. A more powerful convertor can be found on CodePlex in the HTML 2 RTB XAML project, which in its turn is based on HTML Agility Pack. Unfortunately the conversion code in the former project is tightly coupled to (Silverlight) controls. I didn't have the time to refactor this. (Take that as a hint )
Making the RichTextBox understand and speak HTML
The RichTextBox from the Extended WPF Toolkit -again from CodePlex- not only has a Text dependency property for data binding. It also introduces the concept of Text Formatters, allowing to declaratively and programmatically specify the format of the content. The three canonical Text Formatters are included; PlainTextFormatter, RtfFormatter, and XamlFormatter.
All I did was taking the code of the XamlFormatter and plugged in the Html converter. I also replaced the ASCII encoding by UTF8 encoding, since French text wasn't displayed correctly.
Here's the full code of the HTML-to-FlowDocument-and-back converter:
public class HtmlFormatter : ITextFormatter
{
public string GetText(System.Windows.Documents.FlowDocument document)
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream())
{
tr.Save(ms, DataFormats.Xaml);
return HtmlFromXamlConverter.ConvertXamlToHtml(UTF8Encoding.Default.GetString(ms.ToArray()));
}
}
public void SetText(System.Windows.Documents.FlowDocument document, string text)
{
text = HtmlToXamlConverter.ConvertHtmlToXaml(text, false);
try
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
{
tr.Load(ms, DataFormats.Xaml);
}
}
catch
{
throw new InvalidDataException("data provided is not in the correct Html format.");
}
}
}
Architecture
The following UML class diagram reveals all major component of the architecture:
Source Code
The included project contains the control, and a sample client.
Here's how the client application looks like:
Here's the code: U2UConsult.HtmlTextBlock.Sample.zip (91,62 kb)
Enjoy!