Undo and Redo
Ctrl+Z and Ctrl+Y undo and redo changes in the WPF TextControl without any code, because the control records every change in its undo buffer. The API adds what an application needs beyond the keys: undo and redo commands for your own menus, the names of the actions, several changes grouped into one step, and a buffer that can be switched off.
The examples use a window with a TextControl named textControl1, as in Create Your First WPF Application.
How the Undo Buffer Works
- Keys. Ctrl+Z undoes the last action, and Ctrl+Y restores it.
- Changes made by code. Changes through the API are recorded as well, except changes made with the properties and methods of a header or footer, which HeaderFooter.AddUndoUnit adds to the buffer. Every property set on the current selection is a step, while a prepared
Selectionobject assigned once is a single step; see Selections and Caret Position. - Names. UndoActionName and RedoActionName return the name of the step that the next undo or redo affects.
Add Undo and Redo Commands
| Member | Does |
|---|---|
| Undo | Undoes the last action |
| Redo | Restores the last undone action |
| CanUndo, CanRedo | Whether there is an action to undo or redo |
UndoActionName, RedoActionName | The name of that action, or null if there is none |
The following window has an Edit menu with Undo, Redo and a command of its own, Insert Closing, which the next section explains. MainWindow.xaml:
<Window x:Class="SampleApp.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>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Edit" SubmenuOpened="EditMenu_SubmenuOpened">
<MenuItem x:Name="undoItem" Click="Undo_Click" />
<MenuItem x:Name="redoItem" Click="Redo_Click" />
<Separator />
<MenuItem Header="Insert _Closing" Click="InsertClosing_Click" />
</MenuItem>
</Menu>
<WPF:TextControl x:Name="textControl1" />
</DockPanel>
</Window>
MainWindow.xaml.cs, with SampleApp replaced by the namespace of your project:
using System.Windows;
using TXTextControl;
namespace SampleApp;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void EditMenu_SubmenuOpened(object sender, RoutedEventArgs e)
{
// Show which action the next click undoes or redoes
undoItem.IsEnabled = textControl1.CanUndo;
undoItem.Header = textControl1.CanUndo ? "_Undo " + textControl1.UndoActionName : "_Undo";
redoItem.IsEnabled = textControl1.CanRedo;
redoItem.Header = textControl1.CanRedo ? "_Redo " + textControl1.RedoActionName : "_Redo";
}
private void Undo_Click(object sender, RoutedEventArgs e) => textControl1.Undo();
private void Redo_Click(object sender, RoutedEventArgs e) => textControl1.Redo();
private void InsertClosing_Click(object sender, RoutedEventArgs e)
{
// Everything until EndUndoAction is undone in one step
textControl1.BeginUndoAction("Insert Closing");
try
{
textControl1.Selection.Text = "Kind regards,\r\n";
Selection name = new Selection();
name.Text = "Tim Typer";
name.Bold = true;
textControl1.Selection = name;
}
finally
{
textControl1.EndUndoAction();
}
}
}
SubmenuOpened updates the two items every time the menu opens: IsEnabled grays out an item without an action, and the Header shows the name of the action.
Group Several Changes Into One Step
Insert Closing inserts a line of text and a bold name, which would be two separate steps. BeginUndoAction and EndUndoAction turn everything between them into one step:
BeginUndoAction("Insert Closing")opens the group and names it. The name can benullor an empty string if the menu doesn't show it.Selection.Textinserts "Kind regards," and ends the paragraph with "\r\n". The preparedSelectionobject inserts the bold name.EndUndoActionin thefinallyblock closes the group, even when an exception occurs in between.
Run the application, click Edit > Insert Closing and open Edit again: Undo Insert Closing removes both lines with one click. Search and Replace groups all replacements the same way.
Clear the Buffer After Loading a Document
When your code loads a document, for example a template, the user shouldn't be able to undo the loading. Call ClearUndo after Load:
private void LoadTemplate(string path)
{
textControl1.Load(path, StreamType.WordprocessingML);
// The user cannot undo the loading of the template
textControl1.ClearUndo();
}
Switch the Buffer Off
The undo buffer takes time and memory. While your code generates a long document in the editor, such as a report, switch it off with IsUndoEnabled, and switch it on again before the user edits:
textControl1.IsUndoEnabled = false;
try
{
// Build the document here
}
finally
{
// Switch undo on again, even if an exception occurred
textControl1.IsUndoEnabled = true;
}
The finally block makes sure that an exception doesn't leave the editor without undo.
IsUndoEnabled is a dependency property, so it can also be set in XAML (IsUndoEnabled="False") or bound to a view model. The Windows Forms control calls the same setting AllowUndo.
Documents that are created without being shown to a user are built with ServerTextControl instead, which has no undo history; see Document Processing.
Next Steps
- Search and Replace: replace text in one undo step.
- Clipboard Operations: copy, cut and paste.
- Keyboard Shortcuts: all keys of the editor.
- Track Changes: record changes for review instead of undoing them.