Associazione wpf Richtextbox
-
19-08-2019 - |
Domanda
Per eseguire il DataBinding del documento in un RichtextBox WPF , finora ho visto 2 soluzioni, che devono derivare dal RichtextBox e aggiungere una DependencyProperty, e anche la soluzione con un " proxy " . Né il primo né il secondo sono soddisfacenti. Qualcuno conosce un'altra soluzione, o invece, un controllo RTF commerciale in grado di DataBinding ? La normale casella di testo non è un'alternativa, poiché è necessario formattare il testo.
Qualche idea?
Soluzione
So che questo è un vecchio post, ma dai un'occhiata al Toolkit WPF esteso . Ha un RichTextBox che supporta ciò che stai cercando di fare.
Altri suggerimenti
C'è un modo molto più semplice!
Puoi facilmente creare una proprietà DocumentXaml
(o DocumentRTF
) allegata che ti permetterà di associare il documento di RichTextBox. Viene utilizzato in questo modo, dove Autobiografia è una proprietà stringa nel modello di dati:
<TextBox Text="{Binding FirstName}" />
<TextBox Text="{Binding LastName}" />
<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />
Voila! Dati RichTextBox completamente vincolanti!
L'implementazione di questa proprietà è abbastanza semplice: quando la proprietà è impostata, caricare XAML (o RTF) in un nuovo FlowDocument. Quando FlowDocument cambia, aggiorna il valore della proprietà.
Questo codice dovrebbe fare il trucco:
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()));
}
};
}});
}
Lo stesso codice potrebbe essere utilizzato per TextFormats.RTF o TextFormats.XamlPackage. Per XamlPackage avresti una proprietà di tipo byte [] anziché stringa.
Il formato XamlPackage presenta numerosi vantaggi rispetto al semplice XAML, in particolare la possibilità di includere risorse come immagini, ed è più flessibile e più facile da lavorare rispetto a RTF.
È difficile credere che questa domanda sia rimasta per 15 mesi senza che nessuno abbia indicato il modo più semplice per farlo.
Posso darti una soluzione ok e puoi andare con essa, ma prima di farlo proverò a spiegare perché Document non è non per cominciare una DependencyProperty.
Durante la durata di un controllo RichTextBox, la proprietà Document generalmente non cambia. RichTextBox è inizializzato con un FlowDocument. Tale documento viene visualizzato, può essere modificato e modificato in molti modi, ma il valore sottostante della proprietà Document rimane quell'istanza di FlowDocument. Pertanto, non vi è alcun motivo per cui dovrebbe essere una proprietà di dipendenza, ovvero associabile. Se hai più posizioni che fanno riferimento questo FlowDocument, è necessario il riferimento una sola volta. Poiché si tratta della stessa istanza ovunque, le modifiche saranno accessibili a tutti.
Non credo che FlowDocument supporti le notifiche di modifica dei documenti, anche se non ne sono sicuro.
Detto questo, ecco una soluzione. Prima di iniziare, poiché RichTextBox non implementa INotifyPropertyChanged e Document non è una proprietà di dipendenza, non abbiamo notifiche quando la proprietà Document di RichTextBox cambia, quindi l'associazione può essere solo OneWay.
Crea una classe che fornirà FlowDocument. Il bind richiede l'esistenza di una proprietà di dipendenza, quindi questa classe eredita da 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); }
}
}
Crea una finestra con una ricca casella di testo 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>
Assegna alla finestra un campo di tipo HasDocument.
HasDocument hasDocument;
Il costruttore di finestre dovrebbe creare l'associazione.
hasDocument = new HasDocument();
InitializeComponent();
Binding b = new Binding("Document");
b.Source = richTextBox;
b.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(hasDocument, HasDocument.DocumentProperty, b);
Se vuoi essere in grado di dichiarare l'associazione in XAML, dovresti far derivare la tua classe HasDocument da FrameworkElement in modo che possa essere inserita nella struttura logica.
Ora, se dovessi cambiare la proprietà del documento su HasDocument, cambierà anche il documento della casella di testo avanzato.
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;
Ho ottimizzato un po 'il codice precedente. Prima di tutto, il cambio non ha funzionato per me. Dopo aver cambiato range.Changed in richTextBox.TextChanged si scopre che il gestore di eventi TextChanged può invocare SetDocumentXaml in modo ricorsivo, quindi ho fornito protezione contro di esso. Ho anche usato XamlReader / XamlWriter invece di 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));
}
};
}
)
);
}
Perché non usare semplicemente FlowDocumentScrollViewer?
<RichTextBox>
<FlowDocument PageHeight="180">
<Paragraph>
<Run Text="{Binding Text, Mode=TwoWay}"/>
</Paragraph>
</FlowDocument>
</RichTextBox>
Questo sembra di gran lunga il modo più semplice e non viene visualizzato in nessuna di queste risposte.
Nel modello della vista basta avere la variabile Text
.
Crea un UserControl che ha un RichTextBox .. Ora aggiungi la seguente proprietà di dipendenza:
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;
}
Questa soluzione è probabilmente quella "proxy" soluzione che hai visto da qualche parte .. Tuttavia .. RichTextBox semplicemente non ha Document come DependencyProperty ... Quindi devi farlo in un altro modo ...
HTH
Ecco una versione VB.Net della risposta di 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
Fine classe
Questa versione di VB.Net funziona per la mia situazione. Ho rimosso il semaforo della raccolta thread, utilizzando invece RemoveHandler e AddHandler. Inoltre, poiché un FlowDocument può essere associato a un solo RichTextBox alla volta, ho verificato che IsLoaded = True di RichTextBox. Cominciamo con il modo in cui ho usato la classe in un'app MVVM che utilizza ResourceDictionary invece di 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
La maggior parte delle mie esigenze sono state soddisfatte da questa risposta https://stackoverflow.com/a/2989277/3001007 di krzysztof . Ma un problema con quel codice (ho affrontato era), l'associazione non funzionerà con più controlli. Quindi ho modificato _recursionProtection
con un'implementazione basata su Guid
. Quindi funziona anche per più controlli nella stessa finestra.
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));
}
};
}
)
);
}
Per completezza, vorrei aggiungere qualche altra riga della risposta originale https://stackoverflow.com/a/2641774/3001007 di ray-burns . Ecco come utilizzare l'helper.
<RichTextBox local:RichTextBoxHelper.DocumentXaml="{Binding Autobiography}" />
Ragazzi perché preoccuparsi di tutto il faff. Funziona perfettamente. Nessun codice richiesto
<RichTextBox>
<FlowDocument>
<Paragraph>
<Run Text="{Binding Mytextbinding}"/>
</Paragraph>
</FlowDocument>
</RichTextBox>