MVVM Integration Patterns
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
| Part | Holds | Talks to the editor through |
|---|---|---|
| View model | The document as data, settings such as the zoom factor, the commands | Bindings, and an interface for actions that need the control |
| View (MainWindow.xaml and its code-behind) | The TextControl, its bars and the other controls | Its fields, for example textControl1 |
ServerTextControl | A document in memory, without user interface | Nothing: 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:
Documentholds 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.ZoomFactoris an ordinary property; the view binds the dependency property of the same name to it.ExportPdfconverts 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 BinaryStreamTypeAdobePDFconvert. PDF export requires the Professional or Enterprise edition; see Editions and Feature Availability.PrintandFindgo throughIEditorService, 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.EditModeisn't a dependency property, so a binding can't set it. The view listens toPropertyChangedof the view model and sets EditMode itself:ReadAndSelectdisplays the document and lets the user select text, with editing restricted as the document permissions specify;Editallows every change.- Print opens the print dialog box and prints;
truestarts 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
IEditorServicefor each one the view model needs. Inside aWindow, compare their result withTXTextControl.WPF.DialogResult.OK, written in full, becauseDialogResultalone is the property of the window. - Events of the editor other than
Changedreach 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
- Commands and Input Bindings: routed commands and key bindings for the editor.
- Binding Document Content: the attached property and the bindings to
InputFormat. - ServerTextControl: the document engine without user interface.
- MVVM Integration Examples: the sources and samples to build on.