With a normal TextBox
in UWP, you can databind to the Text
property and easily get or set the value from a ViewModel. The RichEditBox
doesn't have a data-bindable Text
property though; instead you have to use the ITextDocument
interface exposed by the Document
property and use various methods to get and set text.
How can I databind the plain text to something in my ViewModel?
It is possible to data-bind the plain-text of a RichEditBox
by using a custom attached property. This attached property handles the conversion between the rich text and the plain text of the document.
Here is an example XAML page, code-behind, and ViewModel showing the usage of the attached property:
Copy this as the content of a new page in your project
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Margin="30">
<RichEditBox local:RichEditBoxExtension.PlainText="{Binding PlainText,
Mode=TwoWay}" x:Name="richedit"/>
<Button Content="Bold selection" Click="MakeBold"/>
<Button Content="Change plain text (view model)" Click="ChangeText"/>
<Button Content="Change rich text (control property)" Click="ChangeRichText"/>
<TextBlock Text="PlainText property is..." />
<TextBlock Text="{Binding PlainText, Mode=OneWay}" />
</StackPanel>
</Grid>
This assumes you're using the default MainPage.xaml.cs
; change the constructor name as appropriate
public MainPage()
{
InitializeComponent();
DataContext = model = new ViewModel();
model.PlainText = "Hello, world";
}
private void ChangeText(object sender, RoutedEventArgs e)
{
model.PlainText = "Here is some plain text";
}
private void ChangeRichText(object sender, RoutedEventArgs e)
{
richedit.Document.SetText(TextSetOptions.None, "Here is some rich text");
var selection = richedit.Document.Selection;
selection.StartPosition = 8;
selection.EndPosition = 12;
selection.CharacterFormat.Underline = UnderlineType.Single;
selection.MoveStart(TextRangeUnit.Word, 1);
selection.Expand(TextRangeUnit.Word);
selection.CharacterFormat.Weight = FontWeights.Bold.Weight;
}
private void MakeBold(object sender, RoutedEventArgs e)
{
richedit.Document.Selection.CharacterFormat.Weight = FontWeights.Bold.Weight;
}
Nothing special; just a single string property. You can put this in its own file, or paste it into the main code-behind file.
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string plainText;
public string PlainText
{
get { return plainText; }
set
{
plainText = value;
RaisePropertyChanged();
}
}
void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
So far, nothing special. The RichEditBox
uses the attached property RichEditBoxExtension.PlainText
and binds it to the ViewModel property PlainText
. There is another TextBlock
on the page to show the current value of the PlainText
property, and a few buttons to manipulate the text.
The implementation of RichEditBoxExtension.PlainText
is pretty straight-forward, but it takes a fair amount of code due to the dependency-property infrastructure and the need to avoid endless property updates (where changing the rich text triggers the plain text, which triggers the rich text which triggers the plain text, and so on and so on).
This can be in its own file or again just pasted into the code-behind file.
public class RichEditBoxExtension
{
// Standard attached property. It mimics the "Text" property of normal text boxes
public static readonly DependencyProperty PlainTextProperty =
DependencyProperty.RegisterAttached("PlainText", typeof(string),
typeof(RichEditBoxExtension), new PropertyMetadata(null, OnPlainTextChanged));
// Standard DP infrastructure
public static string GetPlainText(DependencyObject o)
{
return o.GetValue(PlainTextProperty) as string;
}
// Standard DP infrastructure
public static void SetPlainText(DependencyObject o, string s)
{
o.SetValue(PlainTextProperty, s);
}
private static void OnPlainTextChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
var source = o as RichEditBox;
if (o == null || e.NewValue == null)
return;
// This attaches an event handler for the TextChange event in the RichEditBox,
// ensuring that we're made aware of any changes
AttachRichEditBoxChangingHelper(o);
// To avoid endless property updates, we make sure we only change the RichText's
// Document if the PlainText was modified (vs. if PlainText is responding to
// Document being modified)
var state = GetState(o);
switch (state)
{
case RichEditChangeState.Idle:
var text = e.NewValue as string;
SetState(o, RichEditChangeState.PlainTextChanged);
source.Document.SetText(Windows.UI.Text.TextSetOptions.None, text);
break;
case RichEditChangeState.RichTextChanged:
SetState(o, RichEditChangeState.Idle);
break;
default:
Debug.Assert(false, "Unknown state");
SetState(o, RichEditChangeState.Idle);
break;
}
}
#region Glue
// Trivial state machine to determine who last changed the text properties
enum RichEditChangeState
{
Idle,
RichTextChanged,
PlainTextChanged,
Unknown
}
// Helper class that just stores a state inside a textbox, determining
// whether it is already being changed by code or not
class RichEditChangeStateHelper
{
public RichEditChangeState State { get; set; }
}
// Private attached property (never seen in XAML or anywhere else) to attach
// the state variable for us. Because this isn't used in XAML, we don't need
// the normal GetXXX and SetXXX static methods.
static readonly DependencyProperty RichEditChangeStateHelperProperty =
DependencyProperty.RegisterAttached("RichEditChangeStateHelper",
typeof(RichEditChangeStateHelper), typeof(RichEditBoxExtension), null);
// Inject our state into the textbox, and also attach an event-handler
// for the TextChanged event.
static void AttachRichEditBoxChangingHelper(DependencyObject o)
{
if (o.GetValue(RichEditChangeStateHelperProperty) != null)
return;
var richEdit = o as RichEditBox;
var helper = new RichEditChangeStateHelper();
o.SetValue(RichEditChangeStateHelperProperty, helper);
richEdit.TextChanged += (sender, args) =>
{
// To avoid re-entrancy, make sure we're not already changing
var state = GetState(o);
switch (state)
{
case RichEditChangeState.Idle:
string text = null;
richEdit.Document.GetText(Windows.UI.Text.TextGetOptions.None, out text);
if (text != GetPlainText(o))
{
SetState(o, RichEditChangeState.RichTextChanged);
o.SetValue(PlainTextProperty, text);
}
break;
case RichEditChangeState.PlainTextChanged:
SetState(o, RichEditChangeState.Idle);
break;
default:
Debug.Assert(false, "Unknown state");
SetState(o, RichEditChangeState.Idle);
break;
}
};
}
// Helper to set the state managed by the textbox
static void SetState(DependencyObject o, RichEditChangeState state)
{
(o.GetValue(RichEditChangeStateHelperProperty)
as RichEditChangeStateHelper).State = state;
}
// Helper to get the state managed by the textbox
static RichEditChangeState GetState(DependencyObject o)
{
return (o.GetValue(RichEditChangeStateHelperProperty)
as RichEditChangeStateHelper).State;
}
#endregion
}
The attached property basically does two things, but there's a lot of boilerplate code and state machinery surrounding it:
PlainText
attached property is changed, it updates the RichEditBox
with the plain text using source.Document.SetText(TextSetOptions.None, text)
RichEditBox
text changes (including rich text changes), it updates the PlainText
attached property using richEdit.Document.GetText(TextGetOptions.None, out text)
and then o.SetValue(PlainTextProperty, text)
.Note that this basic approach can be used to data-bind other "derived" properties that you want to compute based off real data-bindable properties.