TX Text Control .NET for WPF

Undo and Redo

Topic

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 Selection object 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

MemberDoes
UndoUndoes the last action
RedoRestores the last undone action
CanUndo, CanRedoWhether there is an action to undo or redo
UndoActionName, RedoActionNameThe 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 be null or an empty string if the menu doesn't show it.
  • Selection.Text inserts "Kind regards," and ends the paragraph with "\r\n". The prepared Selection object inserts the bold name.
  • EndUndoAction in the finally block 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