TX Text Control .NET for WPF

MVVM Integration Patterns

Topic

In a Model-View-ViewModel (MVVM) application, the window binds to a view model that holds the data and the commands, and the view model knows nothing about controls. A word processor fits this pattern with a clear division: the TextControl stays in the view, while the document, the settings and the commands live in the view model. The samples of Text Control are written with code-behind, so no ready-made MVVM sample exists; the example below assembles one from bindings, an attached property and a small interface, and it compiles against version 34.0.

Who Does What

PartHoldsTalks to the editor through
View modelThe document as data, settings such as the zoom factor, the commandsBindings, and an interface for actions that need the control
View (MainWindow.xaml and its code-behind)The TextControl, its bars and the other controlsIts fields, for example textControl1
ServerTextControlA document in memory, without user interfaceNothing: the view model uses it directly

Three kinds of connections carry the traffic between view model and view:

  • Bindings to dependency properties such as ZoomFactor and ViewMode, in both directions.
  • An attached property for the document, which loads the bound byte array into the editor and writes the edited document back. Binding Document Content builds it; this page uses it unchanged.
  • Code-behind for everything else: properties that aren't dependency properties, such as EditMode, and actions that need the control, such as printing or opening a built-in dialog box.

Toolbar controls that follow the formatting at the input position bind to InputFormat with ElementName and don't involve the view model at all.

Write the View Model

The view model uses a minimal ICommand implementation. Add it as RelayCommand.cs:

using System;
using System.Windows.Input;

namespace MyWPFApp
{
    public class RelayCommand : ICommand
    {
        private readonly Action execute;
        private readonly Func<bool> canExecute;

        public RelayCommand(Action execute, Func<bool> canExecute = null)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        // Let WPF ask again whenever the input changes
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return canExecute == null || canExecute();
        }

        public void Execute(object parameter)
        {
            execute();
        }
    }
}

Actions that only the control can carry out are described by an interface, IEditorService.cs, which the window implements:

namespace MyWPFApp
{
    // What the view model may ask of the editor, without knowing the control
    public interface IEditorService
    {
        void Print();
        void ShowFindDialog();
    }
}

The view model itself, EditorViewModel.cs, keeps the document in a file below the local application data folder; replace the two file calls with the database or service of your application:

using System;
using System.ComponentModel;
using System.IO;
using System.Windows.Input;
using TXTextControl;

namespace MyWPFApp
{
    public class EditorViewModel : INotifyPropertyChanged
    {
        private readonly IEditorService editor;
        private readonly string storePath = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "letter.tx");

        private byte[] document;
        private int zoomFactor = 100;
        private bool isReadOnly;
        private bool isDirty;

        public EditorViewModel(IEditorService editor)
        {
            this.editor = editor;

            NewCommand = new RelayCommand(() => { Document = null; IsDirty = false; });
            SaveCommand = new RelayCommand(Save, () => IsDirty);
            ExportPdfCommand = new RelayCommand(ExportPdf, () => document != null);
            PrintCommand = new RelayCommand(() => editor.Print());
            FindCommand = new RelayCommand(() => editor.ShowFindDialog());

            if (File.Exists(storePath))
            {
                document = File.ReadAllBytes(storePath);
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ICommand NewCommand { get; }
        public ICommand SaveCommand { get; }
        public ICommand ExportPdfCommand { get; }
        public ICommand PrintCommand { get; }
        public ICommand FindCommand { get; }

        // The document in the TX Text Control format, bound to the editor
        public byte[] Document
        {
            get { return document; }
            set
            {
                document = value;
                IsDirty = true;
                OnPropertyChanged(nameof(Document));
            }
        }

        // Bound to the ZoomFactor dependency property
        public int ZoomFactor
        {
            get { return zoomFactor; }
            set { zoomFactor = value; OnPropertyChanged(nameof(ZoomFactor)); }
        }

        // Applied to EditMode by the view
        public bool IsReadOnly
        {
            get { return isReadOnly; }
            set { isReadOnly = value; OnPropertyChanged(nameof(IsReadOnly)); }
        }

        public bool IsDirty
        {
            get { return isDirty; }
            private set { isDirty = value; OnPropertyChanged(nameof(IsDirty)); }
        }

        private void Save()
        {
            File.WriteAllBytes(storePath, document ?? Array.Empty<byte>());
            IsDirty = false;
        }

        private void ExportPdf()
        {
            byte[] pdf;

            // Convert without the editor: ServerTextControl has no user interface
            using (ServerTextControl tx = new ServerTextControl())
            {
                tx.Create();
                tx.Load(document, BinaryStreamType.InternalUnicodeFormat);
                tx.Save(out pdf, BinaryStreamType.AdobePDF);
            }

            File.WriteAllBytes(Path.ChangeExtension(storePath, ".pdf"), pdf);
        }

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

The view model works as follows:

  • Document holds the document as a byte array in the TX Text Control format. The attached property sets it whenever the user changes the document, and the setter marks the document as changed, which enables the Save command.
  • ZoomFactor is an ordinary property; the view binds the dependency property of the same name to it.
  • ExportPdf converts the document with a ServerTextControl, the document engine without user interface, which is part of the WPF package. Create comes first; Load and Save with BinaryStreamType AdobePDF convert. PDF export requires the Professional or Enterprise edition; see Editions and Feature Availability.
  • Print and Find go through IEditorService, so the view model calls the editor without a reference to the control, and a unit test can pass in a fake implementation.

Write the View

MainWindow.xaml binds the toolbar, the slider and the editor to the view model:

<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">
    <Window.InputBindings>
        <KeyBinding Key="S" Modifiers="Control" Command="{Binding SaveCommand}" />
    </Window.InputBindings>
    <DockPanel>
        <ToolBar DockPanel.Dock="Top">
            <Button Content="New" Command="{Binding NewCommand}" />
            <Button Content="Save" Command="{Binding SaveCommand}" />
            <Button Content="Export PDF" Command="{Binding ExportPdfCommand}" />
            <Button Content="Print" Command="{Binding PrintCommand}" />
            <Button Content="Find" Command="{Binding FindCommand}" />
            <CheckBox Content="Read only" IsChecked="{Binding IsReadOnly}" />
            <ToggleButton Content="B" Focusable="False"
                          IsChecked="{Binding ElementName=textControl1, Path=InputFormat.Bold, Mode=TwoWay}" />
        </ToolBar>
        <Slider DockPanel.Dock="Bottom" Minimum="50" Maximum="200"
                Value="{Binding ZoomFactor}" />
        <WPF:StatusBar x:Name="statusBar1" DockPanel.Dock="Bottom" />
        <WPF:TextControl x:Name="textControl1"
                         StatusBar="statusBar1"
                         ZoomFactor="{Binding ZoomFactor, Mode=TwoWay}"
                         local:DocumentContent.Data="{Binding Document}" />
    </DockPanel>
</Window>

The code-behind in MainWindow.xaml.cs creates the view model, implements IEditorService and applies the read-only setting:

using System.ComponentModel;
using System.Windows;
using TXTextControl;

namespace MyWPFApp
{
    public partial class MainWindow : Window, IEditorService
    {
        private readonly EditorViewModel viewModel;

        public MainWindow()
        {
            InitializeComponent();

            viewModel = new EditorViewModel(this);
            viewModel.PropertyChanged += ViewModel_PropertyChanged;
            DataContext = viewModel;
        }

        private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // EditMode is not a dependency property: apply it here
            if (e.PropertyName == nameof(EditorViewModel.IsReadOnly))
            {
                textControl1.EditMode = viewModel.IsReadOnly ? EditMode.ReadAndSelect : EditMode.Edit;
            }
        }

        public void Print()
        {
            textControl1.Print("Letter", true);
        }

        public void ShowFindDialog()
        {
            textControl1.Find();
        }
    }
}

The view works as follows:

  • ZoomFactor="{Binding ZoomFactor, Mode=TwoWay}" sets the zoom of the editor from the view model; the slider changes the view model, and the editor follows.
  • local:DocumentContent.Data="{Binding Document}" is the attached property of Binding Document Content. Add its class, DocumentContent.cs, to the project.
  • StatusBar="statusBar1" connects the status bar by name. Such connections are strings in XAML and not part of the view model.
  • EditMode isn't a dependency property, so a binding can't set it. The view listens to PropertyChanged of the view model and sets EditMode itself: ReadAndSelect displays the document and lets the user select text, with editing restricted as the document permissions specify; Edit allows every change.
  • Print opens the print dialog box and prints; true starts the dialog box with the settings of the previous call. Find opens the built-in Find dialog box.

Run the application, type a few words and click Save: the button is disabled again until the next change. Close and start the application, and the saved letter appears in the editor.

Where Code-Behind Remains

Some work stays in the view because it needs the control or its events:

  • Built-in dialog boxes, such as Load without parameters or FontDialog, belong to the editor. Add a method to IEditorService for each one the view model needs. Inside a Window, compare their result with TXTextControl.WPF.DialogResult.OK, written in full, because DialogResult alone is the property of the window.
  • Events of the editor other than Changed reach the view model through the code-behind, which calls a method or sets a property of the view model.
  • The ribbon, the rulers and the status bar are connected to the editor by name in XAML, as in Create a WPF Ribbon Application.

Keep this code-behind thin: it forwards to the view model or to the control, and holds no decisions of its own.

Next Steps