web-dev-qa-db-fra.com

Liaison Richtextbox wpf

Pour effectuer la liaison de données du document dans WPF RichtextBox , j’ai vu jusqu’à présent 2 solutions, qui doivent dériver de RichtextBox et ajouter un DependencyProperty, ainsi que la solution avec "proxy" ..__ ou le second est satisfaisant. Est-ce que quelqu'un connaît une autre solution ou plutôt un contrôle commercial RTF capable de DataBinding ? La zone de texte normale n’est pas une alternative, car nous avons besoin de formater du texte.

Une idée?

66
Alex Maker

Je sais que ceci est un ancien message, mais consultez le WPF Toolkit étendu . Il a un RichTextBox qui prend en charge ce que vous essayez de faire.

19
Brian Lagunas

Il y a un moyen beaucoup plus facile!

Vous pouvez facilement créer une propriété attachée DocumentXaml (ou DocumentRTF) qui vous permettra de lier le document RichTextBox. Il est utilisé comme ceci, où Autobiography est une propriété de chaîne dans votre modèle de données:

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

Voila! Données RichTextBox entièrement liantes!

L'implémentation de cette propriété est assez simple: lorsque la propriété est définie, chargez le XAML (ou le RTF) dans un nouveau FlowDocument. Lorsque le FlowDocument est modifié, mettez à jour la valeur de la propriété.

Ce code devrait faire l'affaire:

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

Le même code pourrait être utilisé pour TextFormats.RTF ou TextFormats.XamlPackage. Pour XamlPackage, vous auriez une propriété de type byte [] au lieu de chaîne.

Le format XamlPackage présente plusieurs avantages par rapport au XAML standard, notamment la possibilité d'inclure des ressources telles que des images. Il est plus souple et plus facile à utiliser que le format RTF.

Il est difficile de croire que cette question a été posée pendant 15 mois sans que personne ne précise la manière simple de le faire.

93
Ray Burns

Je peux vous donner une solution satisfaisante et vous pouvez y aller, mais avant cela, je vais essayer d'expliquer pourquoi Document est pas un DependencyProperty pour commencer. 

Pendant la durée de vie d'un contrôle RichTextBox, la propriété Document ne change généralement pas. RichTextBox est initialisé avec un FlowDocument. Ce document est affiché, peut être édité et modifié de plusieurs façons, mais la valeur sous-jacente de la propriété Document reste celle d'une instance de FlowDocument. Par conséquent, il n'y a vraiment aucune raison pour que cela soit une propriété de dépendance, c'est-à-dire, Bindable. Si vous avez plusieurs emplacements qui référencent Ce FlowDocument, vous n’avez besoin que de la référence une fois. Comme c'est la même instance partout, les modifications seront accessibles à tous.

Je ne pense pas que FlowDocument prenne en charge les notifications de modification de document, bien que je ne sois pas sûr.

Cela étant dit, voici une solution. Avant de commencer, comme RichTextBox n'implémente pas INotifyPropertyChanged et que Document n'est pas une propriété de dépendance, nous ne recevons aucune notification lorsque la propriété Document de RichTextBox change.

Créez une classe qui fournira le FlowDocument. La liaison nécessite l'existence d'une propriété de dépendance. Cette classe hérite donc de 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); }
        }
    }

Créez une fenêtre avec une zone de texte enrichi en 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>

Donnez à la fenêtre un champ de type HasDocument.

HasDocument hasDocument;

Le constructeur de fenêtre doit créer la liaison.

hasDocument = new HasDocument();

InitializeComponent();

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

Si vous souhaitez pouvoir déclarer la liaison en XAML, vous devez faire en sorte que votre classe HasDocument dérive de FrameworkElement afin de pouvoir l'insérer dans l'arborescence logique.

Désormais, si vous deviez modifier la propriété Document sur HasDocument, le document de la zone de texte enrichi sera également modifié.

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;
17
Szymon Rozga

J'ai optimisé un peu le code précédent . Tout d'abord, range.Changed ne fonctionne pas pour moi .. Après avoir changé de gamme. Changé en richTextBox.TextChanged, il s'avère que le gestionnaire d'événements TextChanged peut invoquer SetDocumentXaml de manière récursive , donc j'ai fourni une protection contre cela. J'ai également utilisé XamlReader/XamlWriter au lieu de 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));
                    }
                };
            }
        )
    );
}
13
Krzysztof

Créez un UserControl qui a un RichTextBox .. Maintenant ajoutez la propriété de dépendance suivante:

    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;
        if (e.NewValue == null)
            control.RTB.Document = new FlowDocument(); //Document is not amused by null :)

        control.RTB.Document = document;
    }

Cette solution est probablement la solution "proxy" que vous avez vue quelque part ... Cependant ... RichTextBox n'a tout simplement pas Document comme DependencyProperty ... Vous devez donc procéder de manière différente ...

HTH

8
Arcturus

Pourquoi ne pas simplement utiliser un FlowDocumentScrollViewer? 

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

Cela semble de loin le moyen le plus simple et ne figure dans aucune de ces réponses.

Dans le modèle de vue, n'utilisez que la variable Text

8
FakeCaleb

Voici une version VB.Net de la réponse de Lolo:

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

Classe de fin

1
BSalita

Cette version VB.Net fonctionne dans ma situation. J'ai supprimé le sémaphore de collection de threads à la place en utilisant RemoveHandler et AddHandler. De plus, puisqu'un FlowDocument ne peut être lié qu'à un seul RichTextBox à la fois, j'ai vérifié que le RichTextBox IsLoaded = True. Commençons par la façon dont j'ai utilisé la classe dans une application MVVM qui utilise ResourceDictionary au lieu de Window.

    ' Loaded and Unloaded events seems to be the only way to initialize a control created from a Resource Dictionary
' Loading document here because Loaded is the last available event to create a document
Private Sub Rtb_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
    ' only good place to initialize RichTextBox.Document with DependencyProperty
    Dim rtb As RichTextBox = DirectCast(sender, RichTextBox)
    Try
        rtb.Document = RichTextBoxHelper.GetDocumentXaml(rtb)
    Catch ex As Exception
        Debug.WriteLine("Rtb_Loaded: Message:" & ex.Message)
    End Try
End Sub

' Loaded and Unloaded events seems to be the only way to initialize a control created from a Resource Dictionary
' Free document being held by RichTextBox.Document by assigning New FlowDocument to RichTextBox.Document. Otherwise we'll see an of "Document belongs to another RichTextBox"
Private Sub Rtb_Unloaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
    Dim rtb As RichTextBox = DirectCast(sender, RichTextBox)
    Dim fd As New FlowDocument
    RichTextBoxHelper.SetDocumentXaml(rtb, fd)
    Try
        rtb.Document = fd
    Catch ex As Exception
        Debug.WriteLine("PoemDocument.PoemDocumentView.PoemRtb_Unloaded: Message:" & ex.Message)
    End Try
End Sub

Public Class RichTextBoxHelper
    Inherits DependencyObject

    Public Shared Function GetDocumentXaml(ByVal depObj As DependencyObject) As FlowDocument
        Return depObj.GetValue(DocumentXamlProperty)
    End Function

    Public Shared Sub SetDocumentXaml(ByVal depObj As DependencyObject, ByVal value As FlowDocument)
        depObj.SetValue(DocumentXamlProperty, value)
    End Sub

    Public Shared ReadOnly DocumentXamlProperty As DependencyProperty = DependencyProperty.RegisterAttached("DocumentXaml", GetType(FlowDocument), GetType(RichTextBoxHelper), New FrameworkPropertyMetadata(Nothing, 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)
        Dim rtb As RichTextBox = DirectCast(depObj, RichTextBox)
        If rtb.IsLoaded Then
            RemoveHandler rtb.TextChanged, AddressOf TextChanged
            Try
                rtb.Document = GetDocumentXaml(rtb)
            Catch ex As Exception
                Debug.WriteLine("RichTextBoxHelper.RegisterIt: ex:" & ex.Message)
                rtb.Document = New FlowDocument()
            End Try
            AddHandler rtb.TextChanged, AddressOf TextChanged
        Else
            Debug.WriteLine("RichTextBoxHelper: Unloaded control ignored:" & rtb.Name)
        End If
    End Sub

    ' When a RichTextBox Document changes, update the DependencyProperty so they're in sync.
    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, rtb.Document)
        End If
    End Sub

End Class
0
BSalita

La plupart de mes besoins ont été satisfaits par cette réponse https://stackoverflow.com/a/2989277/3001007 by krzysztof . Mais un problème avec ce code (j'ai fait face était), la liaison ne fonctionnera pas avec plusieurs contrôles. J'ai donc changé _recursionProtection avec une implémentation basée sur Guid. Donc, cela fonctionne aussi pour plusieurs contrôles dans la même fenêtre.

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

Par souci d’exhaustivité, permettez-moi d’ajouter quelques lignes de la réponse originale https://stackoverflow.com/a/2641774/3001007 by ray-burns . Voici comment utiliser l'assistant.

<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />
0
Ajeeb.K.P