uwpricheditbox

How can I databind to the plain-text value of a RichEditBox in UWP?


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?


Solution

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

    XAML

    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>
    

    Code behind

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

    ViewModel

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

    Attached property

    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:

    1. When the PlainText attached property is changed, it updates the RichEditBox with the plain text using source.Document.SetText(TextSetOptions.None, text)
    2. When the 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.