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.

8 comments:

  1. namespace SilverlightApplication1
    {
    public class RichTextXaml
    {
    // Start and end sections
    private static string _xamlStart = "<Section xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"><Paragraph>";
    private static string _xamlEnd = "</Paragraph></Section>";

    // Xaml Attaced Property
    public static readonly DependencyProperty XamlProperty =
    DependencyProperty.RegisterAttached("Xaml", typeof(string), typeof(RichTextXaml), new PropertyMetadata(null, XamlPropertyChanged));

    public static void SetXaml(DependencyObject attached, string value)
    {
    attached.SetValue(XamlProperty, value);
    }

    public static string GetXaml(DependencyObject attached)
    {
    return (string)attached.GetValue(XamlProperty);
    }

    private static void XamlPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    if (d is RichTextBox)
    (d as RichTextBox).Xaml = string.Format("{0}{1}{2}", _xamlStart, e.NewValue, _xamlEnd as string);
    }
    }
    }

    ReplyDelete
  2. Have you tried to do something similar in a Windows Store app yet? RichTextBox doesn't seem to exist and RichTextBlock is not inheritable.

    ReplyDelete
  3. No, has the richtextblock got a xaml property?

    ReplyDelete
  4. I don't see a xaml property.

    ReplyDelete
  5. Might be tricky, I'll try and have a look later

    ReplyDelete
  6. There is a Blocks property that contains the contents of the RichTextBlock, but it is read only. Seems like data binding rich text is something lots of people would want to do.

    ReplyDelete
  7. I found something that seems to work. The RichTextColumns control that is added with some Visual Studio templates does have a RichTextContent property that I was able to bind to. I'm creating a RichTextBlock control in my ViewModel via XamlReader.Load() that I use as the bound property, but I can live with that. This post helped me figure that out http://www.lhotka.net/weblog/SetRichTextIntoRichTextBlockControl.aspx

    ReplyDelete
  8. Thank you very much! You save me!

    ReplyDelete