Binding Document Content
What in a document can a WPF binding reach? Two things: the formatting at the input position, which TX Text Control exposes for bindings, and the document as a whole, which needs a small class of your own because the TextControl has no dependency property for it. Both are shown below, the first in XAML only, the second with an attached property that connects the editor to a byte[] property of a view model.
What Can Be Bound
| Content | Binding | Direction |
|---|---|---|
| Formatting at the input position: font, size, bold, alignment, lists, style | ElementName=textControl1, Path=InputFormat.<property> | Both ways |
| State of the editor, such as whether undo is possible | ElementName=textControl1, Path=CanUndo | From the editor |
| The whole document | An attached property that calls Load and Save | Both ways |
The target of a WPF binding must be a dependency property. The InputFormat properties and CanUndo are sources, so the targets are the properties of your controls, such as IsChecked or Text. TextControl.Text gets or sets the text contents as a string, but it isn't a dependency property and can't be the target of a binding; the formatted document is read and written with Load and Save.
Bind to the Formatting at the Input Position
TextControl.InputFormat returns an InputFormat object that represents all formatting attributes at the current input position. Its properties are updated automatically when the input position changes, and setting one of them formats the selected text. If the selection has no common value for an attribute, the property is null.
The following MainWindow.xaml puts a style list, a font list, bold and italic buttons and undo and redo buttons above the editor:
<Window x:Class="MyWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WPF="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<ComboBox x:Name="styleComboBox" Width="140" IsEditable="True" IsReadOnly="True"
Text="{Binding ElementName=textControl1, Path=InputFormat.StyleName}"
ItemsSource="{Binding ElementName=textControl1, Path=InputFormat.StyleNames}" />
<ComboBox x:Name="fontComboBox" Width="160" IsEditable="True" IsReadOnly="True"
DropDownOpened="FontComboBox_DropDownOpened"
Text="{Binding ElementName=textControl1, Path=InputFormat.FontFamily}" />
<ToggleButton Content="B" Focusable="False"
IsChecked="{Binding ElementName=textControl1, Path=InputFormat.Bold, Mode=TwoWay}" />
<ToggleButton Content="I" Focusable="False"
IsChecked="{Binding ElementName=textControl1, Path=InputFormat.Italic, Mode=TwoWay}" />
<Separator />
<Button Content="Undo" Focusable="False" Click="UndoButton_Click"
IsEnabled="{Binding ElementName=textControl1, Path=CanUndo}" />
<Button Content="Redo" Focusable="False" Click="RedoButton_Click"
IsEnabled="{Binding ElementName=textControl1, Path=CanRedo}" />
</ToolBar>
<WPF:TextControl x:Name="textControl1" />
</DockPanel>
</Window>
Three short handlers go into MainWindow.xaml.cs:
using System;
using System.Windows;
namespace MyWPFApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void FontComboBox_DropDownOpened(object sender, EventArgs e)
{
// Fill the list with the fonts the editor supports
fontComboBox.ItemsSource = textControl1.InputFormat.GetFontFamilies();
}
private void UndoButton_Click(object sender, RoutedEventArgs e)
{
textControl1.Undo();
}
private void RedoButton_Click(object sender, RoutedEventArgs e)
{
textControl1.Redo();
}
}
}
The XAML works as follows:
ElementName=textControl1makes theTextControlthe source, andPathnames the property: StyleName and StyleNames for the formatting styles of the document, FontFamily for the font, Bold and Italic for the buttons.Mode=TwoWayon the toggle buttons sends a click back to the editor, so that the selected text becomes bold or italic.Focusable="False"keeps the keyboard focus, and with it the selection, in the editor when a button is clicked.- GetFontFamilies returns the fonts the editor can use. The handler fills the list when it opens, as in the example of the reference.
- CanUndo and CanRedo enable the buttons, as the quick access toolbar of the TX Words ribbon sample does. Undo and Redo do the work in the
Clickhandlers; Commands and Input Bindings replaces the handlers with commands.
Run the application and move the caret through text with different formatting: the lists and buttons follow it. Properties in twips, such as FontSize, or of the type System.Drawing.Color, such as TextColor, need a converter before a WPF control can show them; see Value Converters.
Bind the Whole Document
A view model that keeps a document, for example a letter loaded from a database, holds it as data, not as a control. An attached property can connect such data to the editor: WPF calls it when the bound value changes, and it loads the value into the TextControl; when the user edits the document, it saves the document and writes it back to the binding. Add the class as DocumentContent.cs to the project:
using System;
using System.Windows;
using TXTextControl;
namespace MyWPFApp
{
public static class DocumentContent
{
// The document as a byte array in the TX Text Control format
public static readonly DependencyProperty DataProperty =
DependencyProperty.RegisterAttached(
"Data",
typeof(byte[]),
typeof(DocumentContent),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnDataChanged));
// True while the editor and the property are being synchronized
private static readonly DependencyProperty IsUpdatingProperty =
DependencyProperty.RegisterAttached(
"IsUpdating", typeof(bool), typeof(DocumentContent), new PropertyMetadata(false));
// True once the Changed handler is connected
private static readonly DependencyProperty IsConnectedProperty =
DependencyProperty.RegisterAttached(
"IsConnected", typeof(bool), typeof(DocumentContent), new PropertyMetadata(false));
public static byte[] GetData(DependencyObject element)
{
return (byte[])element.GetValue(DataProperty);
}
public static void SetData(DependencyObject element, byte[] value)
{
element.SetValue(DataProperty, value);
}
private static void OnDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TXTextControl.WPF.TextControl textControl)
{
return;
}
if (!(bool)textControl.GetValue(IsConnectedProperty))
{
textControl.Changed += TextControl_Changed;
textControl.SetValue(IsConnectedProperty, true);
}
// The new value came from the editor itself
if ((bool)textControl.GetValue(IsUpdatingProperty))
{
return;
}
if (textControl.IsLoaded)
{
LoadDocument(textControl, (byte[])e.NewValue);
}
else
{
// Load the document as soon as the control is ready
textControl.Loaded -= TextControl_Loaded;
textControl.Loaded += TextControl_Loaded;
}
}
private static void TextControl_Loaded(object sender, RoutedEventArgs e)
{
TXTextControl.WPF.TextControl textControl = (TXTextControl.WPF.TextControl)sender;
textControl.Loaded -= TextControl_Loaded;
LoadDocument(textControl, GetData(textControl));
}
private static void LoadDocument(TXTextControl.WPF.TextControl textControl, byte[] data)
{
textControl.SetValue(IsUpdatingProperty, true);
try
{
if (data == null)
{
textControl.ResetContents();
}
else
{
textControl.Load(data, BinaryStreamType.InternalUnicodeFormat);
}
}
finally
{
textControl.SetValue(IsUpdatingProperty, false);
}
}
private static void TextControl_Changed(object sender, EventArgs e)
{
TXTextControl.WPF.TextControl textControl = (TXTextControl.WPF.TextControl)sender;
if ((bool)textControl.GetValue(IsUpdatingProperty))
{
return;
}
// Write the edited document back to the bound property
byte[] data;
textControl.Save(out data, BinaryStreamType.InternalUnicodeFormat);
textControl.SetValue(IsUpdatingProperty, true);
try
{
textControl.SetCurrentValue(DataProperty, data);
}
finally
{
textControl.SetValue(IsUpdatingProperty, false);
}
}
}
}
The view model, LetterViewModel.cs, has the property the editor binds to and a second one that shows the size of the stored document:
using System.ComponentModel;
namespace MyWPFApp
{
public class LetterViewModel : INotifyPropertyChanged
{
private byte[] body;
public event PropertyChangedEventHandler PropertyChanged;
// The letter in the TX Text Control format, as stored in the database
public byte[] Body
{
get { return body; }
set
{
body = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Body)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Size)));
}
}
public int Size
{
get { return body == null ? 0 : body.Length; }
}
}
}
In MainWindow.xaml, the attached property binds the editor to Body. The prefix local maps the namespace of the project:
<Window x:Class="MyWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WPF="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
xmlns:local="clr-namespace:MyWPFApp"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<TextBlock DockPanel.Dock="Bottom" Margin="4"
Text="{Binding Size, StringFormat=Stored document: {0} bytes}" />
<WPF:TextControl x:Name="textControl1"
local:DocumentContent.Data="{Binding Body}" />
</DockPanel>
</Window>
The constructor in MainWindow.xaml.cs sets the view model as the data context:
public MainWindow()
{
InitializeComponent();
DataContext = new LetterViewModel();
}
Run the application and type: the size below the editor changes with every edit, because each change is saved into Body. Assign a byte array to Body, and the editor shows that document.
The code works as follows:
RegisterAttachedcreates a dependency property that any element can carry, here theTextControl.BindsTwoWayByDefaultmakes{Binding Body}work in both directions withoutMode=TwoWay.OnDataChangedruns when the bound value changes. Before the control has been loaded into the window, the editor isn't ready for a document yet (see Control Initialization), so the load waits for theLoadedevent.- Changed occurs when the contents of the document have changed. The handler saves the document and hands it to the binding with
SetCurrentValue, which keeps the binding in place. IsUpdatingmarks the moments in which the class itself loads or writes a value, so that a change doesn't travel back to where it came from.- BinaryStreamType
InternalUnicodeFormatis the TX Text Control format, which keeps everything the editor supports. An empty value,null, clears the editor with ResetContents.
Saving on every change keeps the view model current at all times, and it saves the whole document each time. For long documents, save less often: let the view model ask for the document when it needs it, for example in its Save command, as MVVM Integration Patterns shows.
Choose the Format of the Bound Data
The attached property stores the TX Text Control format, the right choice for documents that users go on editing. Other formats need only two lines changed:
| Stored as | Load and Save with | Property type |
|---|---|---|
| TX Text Control format | BinaryStreamType.InternalUnicodeFormat | byte[] |
| DOCX | BinaryStreamType.WordprocessingML | byte[] |
| RTF | StringStreamType RichTextFormat | string |
| HTML | StringStreamType.HTMLFormat | string |
For a string format, register the property with typeof(string) and use the Load(string, StringStreamType) and Save(out string, StringStreamType) overloads. DOCX requires the Professional or Enterprise edition; see Editions and Feature Availability. What each format keeps is listed in Supported File Formats.
Next Steps
- Value Converters: twips, colors, list formats and zoom factors in bindings.
- MVVM Integration Patterns: the editor in a view with a view model and commands.
- Load and Save Documents: Open and Save with the built-in dialog boxes.
- TextControlDocument: the element of the template that shows the document.