Richtextbox wpf绑定

86
为了在WPF的RichtextBox中进行Document的数据绑定,我目前看到有两种解决方案,一种是从RichtextBox派生并添加一个DependencyProperty,另一种是使用“代理”的解决方案。
无论是第一种还是第二种都不尽如人意。是否有其他解决方案,或者有商业RTF控件可以进行数据绑定?正常的TextBox不是一个选择,因为我们需要文本格式化。
有什么想法吗?

获得奖励的答案:https://dev59.com/tnRC5IYBdhLWcg3wS_EF#48909764 - Thomas Weller
11个回答

117

有一种更简单的方法!

你可以轻松地创建一个附加的DocumentXaml(或DocumentRTF)属性,它将允许你绑定RichTextBox的文档。在你的数据模型中,使用如下代码,其中Autobiography是一个字符串属性:

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

看这里!完全可绑定的RichTextBox数据!

这个属性的实现非常简单:当属性被设置时,将XAML(或RTF)加载到一个新的FlowDocument中。当FlowDocument更改时,更新属性值。

这段代码应该能解决问题:

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()));
                        }
                    };
                }
            });
}

可以使用相同的代码来处理TextFormats.RTF或TextFormats.XamlPackage。对于XamlPackage,你需要一个类型为byte[]的属性而不是string

XamlPackage格式比纯XAML具有几个优点,特别是包含图像等资源的能力,它比RTF更灵活、更易于操作。

难以置信这个问题在15个月内没有人指出如何轻松解决。


6
@Kelly,使用DataFormats.Rtf,这可以解决多个富文本框的问题。 - CharlieShi
17
双向同步在我的情况下无法正常工作(使用Rtf格式)。range.Changed事件从未被调用。 - Patrick
1
@FabianBigler - 你好 - 如果有人有同样的问题 - 你需要在xaml文件中添加xmlns:local声明,该声明将指向该命名空间的位置。 - Bartosz
3
有人能够举个自传的价值的例子吗? - longlostbro
1
@AntonBakulev 谢谢! - longlostbro
显示剩余3条评论

27

我知道这是一个旧帖子,但是请查看扩展WPF工具包。它有一个支持你想要做的功能的RichTextBox。


10
不推荐使用来自Extended WPF Toolkit的RichTextBox,因为它的速度非常慢。 - Kapitán Mlíko
6
你需要意识到这只是带有额外属性的 WPF RichTextBox,对吗? - user288295
3
跳到2017年...WPF工具包中的RichTextBox可以直接处理富文本或纯文本。它似乎比使用下面的辅助方法快很多(如果你只是复制/粘贴它,它会抛出一个异常)。 - DanW
5
他们的免费许可证仅限非商业使用。:/ - Eric

19

我稍微调整了之前的代码。 首先,range.Changed对我没有起作用。 在我将range.Changed更改为richTextBox.TextChanged后,发现TextChanged事件处理程序可以递归调用SetDocumentXaml,因此我提供了保护。我还使用了XamlReader/XamlWriter而不是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));
                    }
                };
            }
        )
    );
}

谢谢Lolo!我也遇到了原始类的问题。这个修复方法对我很有帮助,节省了大量时间! - Mark Bonafe
我发现这个解决方案存在一个小问题。如果视图在调用之间没有关闭和重新创建,则可能多次设置TextChanged的钩子。我只创建了一个视图并通过列表选择进行加载。为了解决这个问题,我创建了一种更典型的方法来挂接TextChanged事件。然后我简单地在挂接之前取消挂接该方法。这确保它只被挂接一次。没有更多的内存泄漏(也没有更慢的运行代码)。 - Mark Bonafe
这是一个不错的工作解决方案,但是根据我的经验,它不能与多个控件一起使用,请参见我的答案 - Ajeeb.K.P
这太棒了,谢谢。但是如果您想以编程方式设置绑定,则无法工作,我认为这是因为设置绑定的线程与通过XAML设置的线程不同。因此,我不得不添加一个SetDocumentXamlFirst方法,它不使用递归保护,并且只有在您首次想要设置值时才会手动调用。 - stuzor
虚拟机中的备份字符串必须是XML格式的FlowDocument,否则加载将失败。例如:<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"><Paragraph Foreground="Red"><Bold>Hello</Bold></Paragraph></FlowDocument> - Istvan Heckl

19

我可以给你一个可行的解决方案,你可以采纳它,但在此之前,我会尝试解释为什么Document本质上并不是一个DependencyProperty

RichTextBox控件的生命周期中,Document属性通常不会改变。 RichTextBox被初始化为一个FlowDocument。该文档被显示出来,可以进行编辑和修改,但是Document属性的基础值仍然是那个FlowDocument实例。因此,它没有理由成为一个DependencyProperty,即可绑定的属性。如果您有多个位置引用了这个FlowDocument,您只需要引用一次。由于它在所有地方都是相同的实例,所以更改将对每个人都可访问。

我认为FlowDocument可能不支持文档更改通知,尽管我不确定。

话虽如此,下面是一个解决方案。在开始之前,由于RichTextBox没有实现INotifyPropertyChangedDocument不是一个DependencyProperty,所以当RichTextBoxDocument属性发生更改时,我们无法接收通知,因此绑定只能是单向的。

创建一个类来提供FlowDocument。绑定需要存在一个DependencyProperty,因此这个类继承自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); }
    }
}

在XAML中创建一个带有富文本框的窗口(Window)

<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>

Window添加一个类型为HasDocument的字段。

HasDocument hasDocument;

窗口构造函数应该创建绑定。

hasDocument = new HasDocument();

InitializeComponent();

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

如果你想在XAML中声明绑定,那么你需要让HasDocument类继承自FrameworkElement,这样它就可以被插入到逻辑树中。

现在,如果你改变了HasDocument上的Document属性,富文本框的Document也会随之改变。

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;

7
感谢好的回答,但有一个小问题:将Document属性设置为依赖属性是有原因的——为了方便使用MVVM模式控制。 - David Veeneman
1
有道理,但我不同意;仅仅因为MVVM在WPF应用程序中被广泛使用,并不意味着WPF的API应该改变以适应它。我们可以以任何方式解决它。这是一种解决方案。我们也可以选择将我们的Rich Text Box封装在一个用户控件中,并在UserControl上定义一个依赖属性。 - Szymon Rozga

15
 <RichTextBox>
     <FlowDocument PageHeight="180">
         <Paragraph>
             <Run Text="{Binding Text, Mode=TwoWay}"/>
          </Paragraph>
     </FlowDocument>
 </RichTextBox>

看起来这似乎是迄今为止最简单的方法,并且没有显示在任何答案中。

在视图模型中,只需要使用Text变量。


1
在我的情况下,这个解决方案将视图模型中Text属性的文本垂直显示,也就是说,每行一个。 - kintela
这就是我所需要的。谢谢! - Christopher Painter
5
这个解决方案与绑定到Text属性的普通文本框有何不同?使用这段代码实际上会关闭支持格式化的富文本框,这样做违背了其设计初衷。 - Daap
这对我来说非常完美。越简单越好! - gcdev
1
这不允许添加多个段落或多个运行。使用RichTextBox的整个原因是为了获得这些功能。 - David Rector

10
创建一个名为RTB的RichTextBox的UserControl。现在添加以下依赖属性:
    public FlowDocument Document
    {
        get { return (FlowDocument)GetValue(DocumentProperty); }
        set { SetValue(DocumentProperty, value); }
    }

    public static readonly DependencyProperty DocumentProperty =
        DependencyProperty.Register("Document", typeof(FlowDocument), typeof(RichTextBoxControl), new PropertyMetadata(OnDocumentChanged));

    private static void OnDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        RichTextBoxControl control = (RichTextBoxControl) d;
        FlowDocument document = e.NewValue as FlowDocument;
        if (document  == null)
        {
            control.RTB.Document = new FlowDocument(); //Document is not amused by null :)
        }
        else
        {
            control.RTB.Document = document;
        }
    }

这个解决方案可能是你在某处看到的“代理”解决方案。但是,RichTextBox没有Document作为DependencyProperty... 所以你必须用另一种方式来实现...

希望能帮到你。


在最后一行中,您使用了“document”,这在我的代码中会引发错误。由于静态方法的原因,它必须是Document的实例。但是什么样的实例呢?我正在设置通过DependencyProperty获取的文档,“Document”。删除“static”将破坏DependencyProperty的最后一个参数。所以我现在陷入了困境。上面的Helper类也没有显示任何文本:( - ecth

5
我的绝大部分需求都能得到这个答案https://dev59.com/tnRC5IYBdhLWcg3wS_EF#2989277的满足,作者是krzysztof。但我面临的一个问题是,该代码(我曾遇到的)无法与多个控件进行绑定。因此,我使用基于Guid的实现来替换_recursionProtection。这使得它也能在同一窗口中使用多个控件。
 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));
                            }
                        };
                }
            )
        );
    }

为了完整起见,让我从原始答案https://dev59.com/tnRC5IYBdhLWcg3wS_EF#2641774中添加几行,由ray-burns提供。这是如何使用助手。
<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />

在尝试了两个得到高赞的建议后,这个最终对我们起作用了。 - Thomas Weller

4

以下是基于Ray Burns答案的解决方案,使用DataBinding和将XAML字符串转换为RichTextBox文档:

ViewModel

    TestText = @"<FlowDocument xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""><Paragraph><Bold>Hello World!</Bold></Paragraph></FlowDocument>";

查看

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

RichTextBoxHelper

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;
                var    xaml        = GetDocumentXaml(richTextBox);
                Stream sm          = new MemoryStream(Encoding.UTF8.GetBytes(xaml));
                richTextBox.Document = (FlowDocument) XamlReader.Load(sm);
                sm.Close();
            }
        }
    );
}

1
这是Lolo答案的VB.Net版本:
Public Class RichTextBoxHelper
Inherits DependencyObject

Private Shared _recursionProtection As New HashSet(Of System.Threading.Thread)()

Public Shared Function GetDocumentXaml(ByVal depObj As DependencyObject) As String
    Return DirectCast(depObj.GetValue(DocumentXamlProperty), String)
End Function

Public Shared Sub SetDocumentXaml(ByVal depObj As DependencyObject, ByVal value As String)
    _recursionProtection.Add(System.Threading.Thread.CurrentThread)
    depObj.SetValue(DocumentXamlProperty, value)
    _recursionProtection.Remove(System.Threading.Thread.CurrentThread)
End Sub

Public Shared ReadOnly DocumentXamlProperty As DependencyProperty = DependencyProperty.RegisterAttached("DocumentXaml", GetType(String), GetType(RichTextBoxHelper), New FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsRender Or FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, Sub(depObj, e)
                                                                                                                                                                                                                                                                                                                    RegisterIt(depObj, e)
                                                                                                                                                                                                                                                                                                                End Sub))

Private Shared Sub RegisterIt(ByVal depObj As System.Windows.DependencyObject, ByVal e As System.Windows.DependencyPropertyChangedEventArgs)
    If _recursionProtection.Contains(System.Threading.Thread.CurrentThread) Then
        Return
    End If
    Dim rtb As RichTextBox = DirectCast(depObj, RichTextBox)
    Try
        rtb.Document = Markup.XamlReader.Parse(GetDocumentXaml(rtb))
    Catch
        rtb.Document = New FlowDocument()
    End Try
    ' When the document changes update the source
    AddHandler rtb.TextChanged, AddressOf TextChanged
End Sub

Private Shared Sub TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs)
    Dim rtb As RichTextBox = TryCast(sender, RichTextBox)
    If rtb IsNot Nothing Then
        SetDocumentXaml(sender, Markup.XamlWriter.Save(rtb.Document))
    End If
End Sub

结束类


1

大家为什么要费尽周折呢?这个方法完美无缺,不需要任何代码。

<RichTextBox>
    <FlowDocument>
        <Paragraph>
            <Run Text="{Binding Mytextbinding}"/>
        </Paragraph>
    </FlowDocument>
</RichTextBox>

1
在我的情况下,如果没有“FlowDocument”标签,它就无法工作。 - Klaonis
RunText 属性不是依赖属性,所以甚至无法编译。只有依赖属性支持此类型的绑定。 - Hopeless
这正是我所需要的。 - Christopher Painter
如果您想要向RichTextBox添加多个段落或多个运行,那么它将无法正常工作。 - David Rector

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接