Richtextbox wpf binding

119,711

Solution 1

Most of my needs were satisfied by this answer https://stackoverflow.com/a/2989277/3001007 by krzysztof. But one issue with that code (i faced was), the binding won't work with multiple controls. So I changed _recursionProtection with a Guid based implementation. So it's working for Multiple controls in same window as well.

 public class RichTextBoxHelper : DependencyObject
    {
        private static List<Guid> _recursionProtection = new List<Guid>();

        public static string GetDocumentXaml(DependencyObject obj)
        {
            return (string)obj.GetValue(DocumentXamlProperty);
        }

        public static void SetDocumentXaml(DependencyObject obj, string value)
        {
            var fw1 = (FrameworkElement)obj;
            if (fw1.Tag == null || (Guid)fw1.Tag == Guid.Empty)
                fw1.Tag = Guid.NewGuid();
            _recursionProtection.Add((Guid)fw1.Tag);
            obj.SetValue(DocumentXamlProperty, value);
            _recursionProtection.Remove((Guid)fw1.Tag);
        }

        public static readonly DependencyProperty DocumentXamlProperty = DependencyProperty.RegisterAttached(
            "DocumentXaml",
            typeof(string),
            typeof(RichTextBoxHelper),
            new FrameworkPropertyMetadata(
                "",
                FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                (obj, e) =>
                {
                    var richTextBox = (RichTextBox)obj;
                    if (richTextBox.Tag != null && _recursionProtection.Contains((Guid)richTextBox.Tag))
                        return;


                    // Parse the XAML to a document (or use XamlReader.Parse())

                    try
                    {
                        string docXaml = GetDocumentXaml(richTextBox);
                        var stream = new MemoryStream(Encoding.UTF8.GetBytes(docXaml));
                        FlowDocument doc;
                        if (!string.IsNullOrEmpty(docXaml))
                        {
                            doc = (FlowDocument)XamlReader.Load(stream);
                        }
                        else
                        {
                            doc = new FlowDocument();
                        }

                        // Set the document
                        richTextBox.Document = doc;
                    }
                    catch (Exception)
                    {
                        richTextBox.Document = new FlowDocument();
                    }

                    // When the document changes update the source
                    richTextBox.TextChanged += (obj2, e2) =>
                        {
                            RichTextBox richTextBox2 = obj2 as RichTextBox;
                            if (richTextBox2 != null)
                            {
                                SetDocumentXaml(richTextBox, XamlWriter.Save(richTextBox2.Document));
                            }
                        };
                }
            )
        );
    }

For completeness sake, let me add few more lines from original answer https://stackoverflow.com/a/2641774/3001007 by ray-burns. This is how to use the helper.

<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />

Solution 2

There is a much easier way!

You can easily create an attached DocumentXaml (or DocumentRTF) property which will allow you to bind the RichTextBox's document. It is used like this, where Autobiography is a string property in your data model:

<TextBox Text="{Binding FirstName}" />
<TextBox Text="{Binding LastName}" />
<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />

Voila! Fully bindable RichTextBox data!

The implementation of this property is quite simple: When the property is set, load the XAML (or RTF) into a new FlowDocument. When the FlowDocument changes, update the property value.

This code should do the trick:

using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public class RichTextBoxHelper : DependencyObject
{
    public static string GetDocumentXaml(DependencyObject obj)
    {
        return (string)obj.GetValue(DocumentXamlProperty);
    }

    public static void SetDocumentXaml(DependencyObject obj, string value)
    {
        obj.SetValue(DocumentXamlProperty, value);
    }

    public static readonly DependencyProperty DocumentXamlProperty =
        DependencyProperty.RegisterAttached(
            "DocumentXaml",
            typeof(string),
            typeof(RichTextBoxHelper),
            new FrameworkPropertyMetadata
            {
                BindsTwoWayByDefault = true,
                PropertyChangedCallback = (obj, e) =>
                {
                    var richTextBox = (RichTextBox)obj;

                    // Parse the XAML to a document (or use XamlReader.Parse())
                    var xaml = GetDocumentXaml(richTextBox);
                    var doc = new FlowDocument();
                    var range = new TextRange(doc.ContentStart, doc.ContentEnd);

                    range.Load(new MemoryStream(Encoding.UTF8.GetBytes(xaml)),
                          DataFormats.Xaml);

                    // Set the document
                    richTextBox.Document = doc;

                    // When the document changes update the source
                    range.Changed += (obj2, e2) =>
                    {
                        if (richTextBox.Document == doc)
                        {
                            MemoryStream buffer = new MemoryStream();
                            range.Save(buffer, DataFormats.Xaml);
                            SetDocumentXaml(richTextBox,
                                Encoding.UTF8.GetString(buffer.ToArray()));
                        }
                    };
                }
            });
}

The same code could be used for TextFormats.RTF or TextFormats.XamlPackage. For XamlPackage you would have a property of type byte[] instead of string.

The XamlPackage format has several advantages over plain XAML, especially the ability to include resources such as images, and it is more flexible and easier to work with than RTF.

It is hard to believe this question sat for 15 months without anyone pointing out the easy way to do this.

Solution 3

I know this is an old post, but check out the Extended WPF Toolkit. It has a RichTextBox that supports what you are tryign to do.

Solution 4

I can give you an ok solution and you can go with it, but before I do I'm going to try to explain why Document is not a DependencyProperty to begin with.

During the lifetime of a RichTextBox control, the Document property generally doesn't change. The RichTextBox is initialized with a FlowDocument. That document is displayed, can be edited and mangled in many ways, but the underlying value of the Document property remains that one instance of the FlowDocument. Therefore, there is really no reason it should be a DependencyProperty, ie, Bindable. If you have multiple locations that reference this FlowDocument, you only need the reference once. Since it is the same instance everywhere, the changes will be accessible to everyone.

I don't think FlowDocument supports document change notifications, though I am not sure.

That being said, here's a solution. Before you start, since RichTextBox doesn't implement INotifyPropertyChanged and Document is not a DependencyProperty, we have no notifications when the RichTextBox's Document property changes, so the binding can only be OneWay.

Create a class that will provide the FlowDocument. Binding requires the existence of a DependencyProperty, so this class inherits from DependencyObject.

class HasDocument : DependencyObject
{
    public static readonly DependencyProperty DocumentProperty =
        DependencyProperty.Register("Document", 
                                    typeof(FlowDocument), 
                                    typeof(HasDocument), 
                                    new PropertyMetadata(new PropertyChangedCallback(DocumentChanged)));

    private static void DocumentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("Document has changed");
    }

    public FlowDocument Document
    {
        get { return GetValue(DocumentProperty) as FlowDocument; }
        set { SetValue(DocumentProperty, value); }
    }
}

Create a Window with a rich text box in XAML.

<Window x:Class="samples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Flow Document Binding" Height="300" Width="300"
    >
    <Grid>
      <RichTextBox Name="richTextBox" />
    </Grid>
</Window>

Give the Window a field of type HasDocument.

HasDocument hasDocument;

Window constructor should create the binding.

hasDocument = new HasDocument();

InitializeComponent();

Binding b = new Binding("Document");
b.Source = richTextBox;
b.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(hasDocument, HasDocument.DocumentProperty, b);

If you want to be able to declare the binding in XAML, you would have to make your HasDocument class derive from FrameworkElement so that it can be inserted into the logical tree.

Now, if you were to change the Document property on HasDocument, the rich text box's Document will also change.

FlowDocument d = new FlowDocument();
Paragraph g = new Paragraph();
Run a = new Run();
a.Text = "I showed this using a binding";
g.Inlines.Add(a);
d.Blocks.Add(g);

hasDocument.Document = d;

Solution 5

I have tuned up previous code a little bit. First of all range.Changed hasn't work for me. After I changed range.Changed to richTextBox.TextChanged it turns out that TextChanged event handler can invoke SetDocumentXaml recursively, so I've provided protection against it. I also used XamlReader/XamlWriter instead of TextRange.

public class RichTextBoxHelper : DependencyObject
{
    private static HashSet<Thread> _recursionProtection = new HashSet<Thread>();

    public static string GetDocumentXaml(DependencyObject obj)
    {
        return (string)obj.GetValue(DocumentXamlProperty);
    }

    public static void SetDocumentXaml(DependencyObject obj, string value)
    {
        _recursionProtection.Add(Thread.CurrentThread);
        obj.SetValue(DocumentXamlProperty, value);
        _recursionProtection.Remove(Thread.CurrentThread);
    }

    public static readonly DependencyProperty DocumentXamlProperty = DependencyProperty.RegisterAttached(
        "DocumentXaml", 
        typeof(string), 
        typeof(RichTextBoxHelper), 
        new FrameworkPropertyMetadata(
            "", 
            FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            (obj, e) => {
                if (_recursionProtection.Contains(Thread.CurrentThread))
                    return;

                var richTextBox = (RichTextBox)obj;

                // Parse the XAML to a document (or use XamlReader.Parse())

                try
                {
                    var stream = new MemoryStream(Encoding.UTF8.GetBytes(GetDocumentXaml(richTextBox)));
                    var doc = (FlowDocument)XamlReader.Load(stream);

                    // Set the document
                    richTextBox.Document = doc;
                }
                catch (Exception)
                {
                    richTextBox.Document = new FlowDocument();
                }

                // When the document changes update the source
                richTextBox.TextChanged += (obj2, e2) =>
                {
                    RichTextBox richTextBox2 = obj2 as RichTextBox;
                    if (richTextBox2 != null)
                    {
                        SetDocumentXaml(richTextBox, XamlWriter.Save(richTextBox2.Document));
                    }
                };
            }
        )
    );
}
Share:
119,711

Related videos on Youtube

Alex Maker
Author by

Alex Maker

Updated on February 19, 2022

Comments

  • Alex Maker
    Alex Maker about 2 years

    To do DataBinding of the Document in a WPF RichtextBox, I saw 2 solutions so far, which are to derive from the RichtextBox and add a DependencyProperty, and also the solution with a "proxy".

    Neither the first or the second are satisfactory. Does somebody know another solution, or instead, a commercial RTF control which is capable of DataBinding? The normal TextBox is not an alternative, since we need text formatting.

    Any idea?

  • David Veeneman
    David Veeneman almost 14 years
    +1 for the good answer, but one quibble: There is a reason to make the Document property a dependency property--to facilitate using the control with the MVVM pattern.
  • Kelly
    Kelly almost 14 years
    If you have multiple RichTextBoxes on the same window, this solution will not work.
  • Szymon Rozga
    Szymon Rozga almost 14 years
    Fair point, but I disagree; just because MVVM is used widely in WPF apps doesn't mean that WPF's API should change just to accomodate it. We work around it in whatever way we can. This is one solution. We may also simply choose to encapsulate our Rich Text Box in a user control and have a dependency property defined on the UserControl.
  • CharlieShi
    CharlieShi over 11 years
    @Kelly, use DataFormats.Rtf, this can solve multiple richtextboxes issue.
  • Kapitán Mlíko
    Kapitán Mlíko about 11 years
    RichTextBox from Extended WPF Toolkit is really slow I wouldn't recommend it.
  • Patrick
    Patrick about 11 years
    Two-way isn't working for me (using Rtf). The range.Changed event is never getting called.
  • Admin
    Admin almost 10 years
    @ViktorLaCroix you do realize this is just the WPF RichTextBox with an extra property on it right?
  • Klaonis
    Klaonis over 9 years
    In my case, it doesn't work without 'FlowDocument' tag.
  • Mark Bonafe
    Mark Bonafe over 9 years
    Thanks Lolo! I was having problems with the original class, too. This fixed it for me. Huge time saver!
  • Mark Bonafe
    Mark Bonafe over 9 years
    I have found a small problem with this solution. It is possible to have the hook for TextChanged get set multiple times if the view is not closed and recreated between calls. I am creating a single view once and loading via a list selection. To fix this, I created a more typical method for hooking the TextChanged event. Then I simply unhook the method before hooking it. This ensures it is only hooked once. No more memory leak (and no more slow running code).
  • Bartosz
    Bartosz about 9 years
    @FabianBigler - Hi - in case anybody has the same problem - you need to add the xmlns:local declaration to your xaml file that will point to the namespace where this is available
  • Hopeless
    Hopeless over 8 years
    Text property of Run is not dependency property so this does not even compile. Only dependency properties support binding like this.
  • ecth
    ecth almost 8 years
    In the last line you use "document" which throws an error in my code. It has to be an instance of Document because of the static method. But instance of what? I am setting the document I get through the DependencyProperty, the "Document". Removing the "static" breaks the last argument of the DependencyProperty. So here I'm stuck. The Helper-class from above doesn't show any text either :(
  • longlostbro
    longlostbro about 7 years
    can someone give an example of the value of Autobiography?
  • DanW
    DanW over 6 years
    (jump ahead to 2017...) the wpf toolkits RichTextBox works with rich text or plain text right out of the box. It also seems a lot faster than using the helper method below (which throws an exception if you just copy/paste it)
  • Ajeeb.K.P
    Ajeeb.K.P about 6 years
    This is a nice working solution, how ever, in my experience it's not working with multiple controls see my answer.
  • Anton Bakulev
    Anton Bakulev over 5 years
    @longlostbro, Here it is an example of the value: gist.github.com/bakulev/614a665241cc4fd69409e1ab3e03a9ba
  • kintela
    kintela over 5 years
    In my case this solution shows the text of the Text property in the View Model in vertical, that is, one for in each row.
  • Christopher Painter
    Christopher Painter about 5 years
    This was exactly what I needed.
  • Christopher Painter
    Christopher Painter about 5 years
    It's all I needed. Thanks!
  • longlostbro
    longlostbro about 5 years
    @AntonBakulev Thanks!
  • stuzor
    stuzor almost 5 years
    This is great, thanks. Doesn't work if you want to set the binding programmatically though, I assume because the thread setting the binding is different than that which sets via XAML. So I had to add a SetDocumentXamlFirst method, which doesn't use the recursion protection and would only be called manually when you first want to set the value.
  • Daap
    Daap over 4 years
    How is this solution different from a regular TextBox bound to the Text property? It defeats the purpose of a rich text box supporting formatting that is effectively switched off using this code.
  • gcdev
    gcdev over 4 years
    This was perfect for me. The simpler the better!
  • Eric
    Eric about 4 years
    Their free license is for non-commercial use only. :/
  • Istvan Heckl
    Istvan Heckl over 3 years
    The backing string (Autobiography) in the VM must be a FlowDocument in xml format. For example: <FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presenta‌​tion"><Paragraph Foreground="Red"><Bold>Hello</Bold></Paragraph></FlowDocumen‌​t>
  • Istvan Heckl
    Istvan Heckl over 3 years
    The backing string in the VM must be a FlowDocument in xml format else the Load will fail. For example: <FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presenta‌​tion"><Paragraph Foreground="Red"><Bold>Hello</Bold></Paragraph></FlowDocumen‌​t>
  • David Rector
    David Rector over 2 years
    This doesn't allow adding multiple paragraphs or multiple runs. The whole reason to use a rich text box is to get to those features.
  • David Rector
    David Rector over 2 years
    It won't work if you want to add multiple paragraphs or multiple runs to the rich text box.
  • Thomas Weller
    Thomas Weller about 2 years
    After trying 2 highly upvoted suggestions, this finally worked for us.