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:
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;
}
}));
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();
}
}));
});
foreach (TextBox item in new TextBox[] { textBoxSizeX, textBoxSizeY, textBoxColor, })
redirectUndoRedo(item);
}
bool canUndo() { }
void undo() { }
bool canRedo() { }
void redo() { }
}
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:
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;
}
}));
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();
}
}));
});
foreach (TextBox item in new TextBox[] { textBoxSizeX, textBoxSizeY, textBoxColor, })
redirectUndoRedo(item);
}
bool canUndo() { }
void undo() { }
bool canRedo() { }
void redo() { }
}
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.