How to bind a TextBlock to a resource containing formatted text?
Here is my modified code for recursively format text. It handles Bold, Italic, Underline and LineBreak but can easily be extended to support more (modify the switch statement).
public static class MyBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(MyBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);
// Check for grouping
if (sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.
// Check for token
if (GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart) ?
null :
section.Substring(token.Length, section.Length - 1 - token.Length * 2);
switch (token)
{
case "<Bold>":
return new Bold(Traverse(content));
case "<Italic>":
return new Italic(Traverse(content));
case "<Underline>":
return new Underline(Traverse(content));
case "<LineBreak/>":
return new LineBreak();
default:
return new Run(section);
}
}
else return new Run(section);
}
else // Group together
{
Span span = new Span();
foreach (string section in sections)
span.Inlines.Add(Traverse(section));
return span;
}
}
/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;
startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");
// No token here
if (startIndex < 0)
return false;
// No token here
if (startTokenEndIndex < 0)
return false;
token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);
// Check for closed token. E.g. <LineBreak/>
if (token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}
string endToken = token.Insert(1, "/");
// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);
if (temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if (temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;
} while (nesting > 0);
endIndex = pos;
return true;
}
/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
static string[] SplitIntoSections(string value)
{
List<string> sections = new List<string>();
while (!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;
// Check if this is a token section
if (GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if (tokenStartIndex > 0)
sections.Add(value.Substring(0, tokenStartIndex));
sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{ // No tokens, just add the text
sections.Add(value);
value = null;
}
}
return sections.ToArray();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Edit: (proposed by Spook)
A shorter version, but requires the text to be XML-valid:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Xml;
// (...)
public static class TextBlockHelper
{
#region FormattedText Attached dependency property
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(TextBlockHelper),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if (textBlock != null)
{
textBlock.Inlines.Clear();
textBlock.Inlines.Add(Process(value));
}
}
#endregion
static Inline Process(string value)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(value);
Span span = new Span();
InternalProcess(span, doc.ChildNodes[0]);
return span;
}
private static void InternalProcess(Span span, XmlNode xmlNode)
{
foreach (XmlNode child in xmlNode)
{
if (child is XmlText)
{
span.Inlines.Add(new Run(child.InnerText));
}
else if (child is XmlElement)
{
Span spanItem = new Span();
InternalProcess(spanItem, child);
switch (child.Name.ToUpper())
{
case "B":
case "BOLD":
Bold bold = new Bold(spanItem);
span.Inlines.Add(bold);
break;
case "I":
case "ITALIC":
Italic italic = new Italic(spanItem);
span.Inlines.Add(italic);
break;
case "U":
case "UNDERLINE":
Underline underline = new Underline(spanItem);
span.Inlines.Add(underline);
break;
}
}
}
}
}
And an example of usage:
<RootItem xmlns:u="clr-namespace:MyApp.Helpers">
<TextBlock u:TextBlockHelper.FormattedText="{Binding SomeProperty}" />
</RootItem>
How about using attached behavior? Below code only handles bold tags. Each word which should be bold needs to be wrapped in bold tags. You probably want to make the class accept other formats as well. Also spaces needs to be handled better, the class strips out consecutive spaces and add one extra to the end. So consider below class as demo code only which will need further work to be useful but it should get you started.
XAML:
<Window x:Class="FormatTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:FormatTest="clr-namespace:FormatTest"
Title="Window1" Height="300" Width="300">
<TextBlock FormatTest:FormattedTextBehavior.FormattedText="{Binding Path=Text}" />
</Window>
Code behind:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace FormatTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = this;
}
public string Text { get { return "Some <Bold>formatted</Bold> text."; } }
}
public static class FormattedTextBehavior
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBehavior),
new UIPropertyMetadata("", FormattedTextChanged));
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = sender as TextBlock;
string value = e.NewValue as string;
string[] tokens = value.Split(' ');
foreach (string token in tokens)
{
if (token.StartsWith("<Bold>") && token.EndsWith("</Bold>"))
{
textBlock.Inlines.Add(new Bold(new Run(token.Replace("<Bold>", "").Replace("</Bold>", "") + " ")));
}
else
{
textBlock.Inlines.Add(new Run(token + " "));
}
}
}
}
}
I've added hyperlink and image support to Vincents solution:
public static class FormattedTextBlock
{
public static string GetFormattedText(DependencyObject obj)
{
return (string)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, string value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached("FormattedText",
typeof(string),
typeof(FormattedTextBlock),
new UIPropertyMetadata("", FormattedTextChanged));
static Inline Traverse(string value)
{
// Get the sections/inlines
string[] sections = SplitIntoSections(value);
// Check for grouping
if(sections.Length.Equals(1))
{
string section = sections[0];
string token; // E.g <Bold>
int tokenStart, tokenEnd; // Where the token/section starts and ends.
// Check for token
if(GetTokenInfo(section, out token, out tokenStart, out tokenEnd))
{
// Get the content to further examination
string content = token.Length.Equals(tokenEnd - tokenStart) ?
null :
section.Substring(token.Length, section.Length - 1 - token.Length * 2);
switch(token.ToUpper())
{
case "<B>":
case "<BOLD>":
/* <b>Bold text</b> */
return new Bold(Traverse(content));
case "<I>":
case "<ITALIC>":
/* <i>Italic text</i> */
return new Italic(Traverse(content));
case "<U>":
case "<UNDERLINE>":
/* <u>Underlined text</u> */
return new Underline(Traverse(content));
case "<BR>":
case "<BR/>":
case "<LINEBREAK/>":
/* Line 1<br/>line 2 */
return new LineBreak();
case "<A>":
case "<LINK>":
/* <a>{http://www.google.de}Google</a> */
var start = content.IndexOf("{");
var end = content.IndexOf("}");
var url = content.Substring(start + 1, end - 1);
var text = content.Substring(end + 1);
var link = new Hyperlink();
link.NavigateUri = new System.Uri(url);
link.RequestNavigate += Hyperlink_RequestNavigate;
link.Inlines.Add(text);
return link;
case "<IMG>":
case "<IMAGE>":
/* <image>pack://application:,,,/ProjectName;component/directory1/directory2/image.png</image> */
var image = new Image();
var bitmap = new BitmapImage(new Uri(content));
image.Source = bitmap;
image.Width = bitmap.Width;
image.Height = bitmap.Height;
var container = new InlineUIContainer();
container.Child = image;
return container;
default:
return new Run(section);
}
}
else return new Run(section);
}
else // Group together
{
Span span = new Span();
foreach(string section in sections)
span.Inlines.Add(Traverse(section));
return span;
}
}
static void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(e.Uri.ToString());
}
/// <summary>
/// Examines the passed string and find the first token, where it begins and where it ends.
/// </summary>
/// <param name="value">The string to examine.</param>
/// <param name="token">The found token.</param>
/// <param name="startIndex">Where the token begins.</param>
/// <param name="endIndex">Where the end-token ends.</param>
/// <returns>True if a token was found.</returns>
static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex)
{
token = null;
endIndex = -1;
startIndex = value.IndexOf("<");
int startTokenEndIndex = value.IndexOf(">");
// No token here
if(startIndex < 0)
return false;
// No token here
if(startTokenEndIndex < 0)
return false;
token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1);
// Check for closed token. E.g. <LineBreak/>
if(token.EndsWith("/>"))
{
endIndex = startIndex + token.Length;
return true;
}
string endToken = token.Insert(1, "/");
// Detect nesting;
int nesting = 0;
int temp_startTokenIndex = -1;
int temp_endTokenIndex = -1;
int pos = 0;
do
{
temp_startTokenIndex = value.IndexOf(token, pos);
temp_endTokenIndex = value.IndexOf(endToken, pos);
if(temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex)
{
nesting++;
pos = temp_startTokenIndex + token.Length;
}
else if(temp_endTokenIndex >= 0 && nesting > 0)
{
nesting--;
pos = temp_endTokenIndex + endToken.Length;
}
else // Invalid tokenized string
return false;
} while(nesting > 0);
endIndex = pos;
return true;
}
/// <summary>
/// Splits the string into sections of tokens and regular text.
/// </summary>
/// <param name="value">The string to split.</param>
/// <returns>An array with the sections.</returns>
static string[] SplitIntoSections(string value)
{
List<string> sections = new List<string>();
while(!string.IsNullOrEmpty(value))
{
string token;
int tokenStartIndex, tokenEndIndex;
// Check if this is a token section
if(GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex))
{
// Add pretext if the token isn't from the start
if(tokenStartIndex > 0)
sections.Add(value.Substring(0, tokenStartIndex));
sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex));
value = value.Substring(tokenEndIndex); // Trim away
}
else
{ // No tokens, just add the text
sections.Add(value);
value = null;
}
}
return sections.ToArray();
}
private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
string value = e.NewValue as string;
TextBlock textBlock = sender as TextBlock;
if(textBlock != null)
textBlock.Inlines.Add(Traverse(value));
}
}
Thanks Vincent for the great template, it works like a charm!