Wednesday 7 September 2011

RichTextBlock XAML Bindable Rich Text Block

I recently needed to use a RichTextBox for a project, but was disapointed to find there is no way of binding the content of the control. Luckily it's possible to access the XAML property and inject RichText format XAML into it. I decided to create a control derived from RichTextBox with a dependency property  used to bind the Rich Text XAML.

The first step is to create the control (I re-exposed the mouse click event as the RichTextBox buries it):

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;

using System.Windows.Ink;
using System.Windows.Input;

namespace RichTextBlock
{
  /// <summary>
  /// Used for databinding to Xaml property via new XamlSource DP
  /// </summary>
  public class RichXamlTextBlock : RichTextBox
  {
    public event MouseButtonEventHandler MouseClicked = null;
    private static string _xamlStart = "<Section xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"><Paragraph>";
    private static string _xamlEnd = "</Paragraph></Section>";

    public RichXamlTextBlock()
      : base()
    {
      base.Cursor = Cursors.Arrow;
    }

    #region XamlSource

    public static readonly DependencyProperty XamlSourceProperty = DependencyProperty.Register("XamlSource", typeof(string), typeof(RichXamlTextBlock),
        new PropertyMetadata(null, new PropertyChangedCallback(OnXamlSourcePropertyChanged)));

    public string XamlSource
    {
      get { return (string)GetValue(XamlSourceProperty); }
      set
      {
        SetValue(XamlSourceProperty, value);
      }
    }

    private static void OnXamlSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      RichXamlTextBlock rtb = (RichXamlTextBlock)d;

      rtb.Xaml = string.Format("{0}{1}{2}", _xamlStart, e.NewValue, _xamlEnd as string);
    }

    #endregion

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
      if (this.MouseClicked != null)
      {
        this.MouseClicked(this, e);
      }
    }
  }
}

The next step is to create a View Model with some XAML rich text. It is important to be careful what you bind as you could end up doing an 'injection attack' on your markup by accident!

using System;

namespace RichTextBlock
{
    public class ViewModel
    {
        private string _xamlSource = "This is a demonstration of the <Run Text=\"RichTextBlock\" FontWeight=\"Bold\" Foreground=\"Green\"/>. The control's XAML can be data bound unlike a normal <Run Text=\"RichTextBox\" FontWeight=\"Bold\" Foreground=\"Red\"/>.";

        public string XamlSource
        {
            get { return this._xamlSource; }
        }
    }
}

Next the new control needs adding to a view:

<UserControl x:Class="RichTextBlock.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:RichTextBlock"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
   
    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>
   
    <UserControl.Resources>
       
        <Style x:Key="RichXamlTextBlockStyle" TargetType="local:RichXamlTextBlock">
            <Setter Property="IsReadOnly" Value="True" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:RichXamlTextBlock">
                        <Grid x:Name="ContentElement" Background="{TemplateBinding Background}" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
       
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" Background="White">
        <local:RichXamlTextBlock XamlSource="{Binding Path=XamlSource, Mode=OneWay}" Style="{StaticResource RichXamlTextBlockStyle}" />
    </Grid>
</UserControl>

I also added a stripped down template to remove the TextBox type appearance to get it looking like a TextBlock.