Click here to Skip to main content
15,879,082 members
Articles / Desktop Programming / WPF
Tip/Trick

Undo/Redo Redirection for WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
6 Apr 2017CPOL5 min read 13.9K   352   3  
Focused TextBox blocks Undo/Redo invocation of other components, but the problem can be solved comprehensively

Epigraphs:

A reformer knows neither how to do nor to undo

José Bergamín

I think multiple levels of undo would be wonderful, too

Bill Joy

Contents

Why It Is a Problem?

There are some very obvious ways to face the problem which make it very likely. Suppose you eventually add some Undo or Undo/Redo behavior at some level of the application. The object being edited can be at the level of some Window or the application singleton. For the purpose of discussion, let’s call it Editor. Now, it’s quite natural and practical to reuse existing commands, such as System.Windows.Input.ApplicationCommands.Redo or ApplicationCommands.Undo, because this would be the way to save time make sure the commands come with standard key gesture, UI text, and so on.

And here where the problem comes into play. It’s also very likely that the UI already has at least one instance of, typically, TextBox. When this control has focus, it completely blocks the Undo or Undo/Redo of the Editor, by a pretty obvious reason: it has its own Undo/Redo handling.

See also: System.Windows.Input.ApplicationCommands, Commanding Overview, Input Overview.

Why I Think This Tip/Trick Can be Useful?

I just faced with this problem during UI development and immediately tried to find the solutions in the Web forums. Even though I knew exactly what to look for and found all relevant techniques quite quickly, I also found a lot of incorrect “solutions” which could not work in principle. At best, such solutions simply disabled Undo/Redo in some UIElement, usually TextBox (because almost all problems of this sort come from TextBox), but still did not allow upper-level component’s Undo/Redo.

Even though finding right solution was quite trivial and simple, I wish everyone skip all those false-positive solutions. There is a good number of the ways to disable Undo or Redo, but there is no point in wasting time on those ways, because they are irrelevant to the goal.

The Solution

First of all, it’s important to decide: what to do with this problem?

Most obvious solution would be to disable TextBox Undo/Redo handling completely, because it cannot be more valuable than the upper-level and application-specific Undo/Redo. After all, such text boxes usually carry very short text. Well, this would be a very reasonable decision. We only need to intercept the commands in each TextBox instance and invoke Undo or Redo action we need.

The solution would be very generic and simple:

C#
public partial class MainWindow : Window {

    public MainWindow() {
        InitializeComponent();
        var redirectUndoRedo = new Action<UIElement>((element) => {
            CommandManager.AddPreviewCanExecuteHandler(
                element,
                new CanExecuteRoutedEventHandler((sender, eventArgs) => {
                    if (eventArgs.Command == ApplicationCommands.Undo ||
                        eventArgs.Command == ApplicationCommands.Redo) {
                            eventArgs.CanExecute = true;
                } //if
            }));
            CommandManager.AddPreviewExecutedHandler(
                element,
                new ExecutedRoutedEventHandler((sender, eventArgs) => {
                    if (eventArgs.Command == ApplicationCommands.Undo ||
                        eventArgs.Command == ApplicationCommands.Redo)
                            eventArgs.Handled = true;
                    if (eventArgs.Command == ApplicationCommands.Undo) {
                        if (this.canUndo())
                            this.undo();
                        eventArgs.Handled = true;
                    } else if (eventArgs.Command == ApplicationCommands.Redo) {
                        if (this.canRedo())
                            this.redo();
                    } //if
                }));
        }); //redirectUndoRedo
        foreach (TextBox item in new TextBox[] { textBoxSizeX, textBoxSizeY, textBoxColor, })
            redirectUndoRedo(item);
        // ...
    } //MainWindow

    bool canUndo() { /* ... */ }
    void undo() { /* ... */ }
    bool canRedo() { /* ... */ }
    void redo() { /* ... */ }

    // ...

} //class MainWindow

I don’t even want to consider such insane idea as doing Undo on both objects, such as TextBox and your application-specific component (editor).

But another more advanced idea could be considered as quite reasonable: you can simply give the priority to your component. When it can Undo or Redo, you perform the operation, if not, you perform the operation on the currently focused TextEditor instance, if any. The solution would be less generic, specific to the control redirecting the operation:

C#
public partial class MainWindow : Window {

    public MainWindow() {
        InitializeComponent();
        var redirectUndoRedo =
            new Action<System.Windows.Controls.Primitives.TextBoxBase>((element) => {
                CommandManager.AddPreviewCanExecuteHandler(
                    element,
                    new CanExecuteRoutedEventHandler((sender, eventArgs) => {
                        if (eventArgs.Command == ApplicationCommands.Undo ||
                            eventArgs.Command == ApplicationCommands.Redo) {
                                eventArgs.CanExecute = true;
                        } //if
                }));
                CommandManager.AddPreviewExecutedHandler(
                    element,
                    new ExecutedRoutedEventHandler((sender, eventArgs) => {
                        if (eventArgs.Command == ApplicationCommands.Undo ||
                            eventArgs.Command == ApplicationCommands.Redo)
                                eventArgs.Handled = true;
                        if (eventArgs.Command == ApplicationCommands.Undo) {
                            if (this.canUndo())
                                this.undo();
                            else
                                element.Undo();
                            eventArgs.Handled = true;
                        } else if (eventArgs.Command == ApplicationCommands.Redo) {
                            if (this.canRedo())
                                this.redo();
                            else
                                element.Redo();
                        } //if
                    }));
        }); //redirectUndoRedo
        foreach (TextBox item in new TextBox[] { textBoxSizeX, textBoxSizeY, textBoxColor, })
            redirectUndoRedo(item);
        // ...
    } //MainWindow

    bool canUndo() { /* ... */ }
    void undo() { /* ... */ }
    bool canRedo() { /* ... */ }
    void redo() { /* ... */ }

    // ...

} //class MainWindow

Generalizations

The problem can appear for any other well-known command and solved in exact same way.

The main decision is the choice between a custom command and the well-known commands, such as those provided by ApplicationCommands, MediaCommands, NavigationCommands, ComponentCommands, EditingCommands and the like.

With custom commands, each component type can be given a separate System.Windows.Input.RoutedUICommand object. That would completely eliminate the problem.

Simple common sense should tell us that if the commands are very application-specific, and when the number in the commands in the application is considerable, custom commands are more desirable and, in particular, maintainable.

If there are no too many commands, or when some of the commands are close to some well-known commands in semantic, reusing of well-known commands may pay off, because it needs a bit less code, may reduce development time and reduce the number of mistakes. However, in this case, a little patch may be needed, such as the one explained above.

Other Demonstrated Techniques

I just want to list some problems demonstrated in the source code provided with the present article, but unrelated to its topic.

On problem is the styling of the menu. Menu objects don’t inherit the properties of the parent window, in this case, font family and size. The solution of this problem is shown.

Another technique is the use of character glyphs in the role of menu item Icon objects. This technique is very simple but requires some special styling. It eliminates the boredom of creation of the bitmap images.

Just look at the only XAML in the sample project.

Compatibility and Build

As the code is based on WPF, I used the first platform version decently compatible with WPF — Microsoft.NET v.3.5. Correspondingly, I provided a solution and a project for Visual Studio 2008. I’ve done it intentionally, to cover all the readers who could use WPF. Later .NET versions will be supported; later versions of Visual Studio can automatically upgrade solution and project files.

In fact, Visual Studio is not required for the build. The code can be built as batch, by using the provided batch file “build.bat”. If your Windows installation directory is different from the default, the build will still work. If .NET installation directory is different from default one, please see the content of this batch file and the comment in its first line — next line can be modified to get the build.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Physics, physical and quantum optics, mathematics, computer science, control systems for manufacturing, diagnostics, testing, and research, theory of music, musical instruments… Contact me: https://www.SAKryukov.org

Comments and Discussions

 
-- There are no messages in this forum --