A simple framework for adding undo/redo suppo...

来源:百度文库 编辑:神马文学网 时间:2024/04/29 13:52:34

4,470,418 members and growing!   6,997 now online. Email Password Remember me? Password problem?
HomeMFC/C++C#ASP.NETVB.NETArchitectSQLAll Topics  Help!ArticlesMessage BoardsLounge
All Topics,C#,.NET >>C# Programming >>Windows Forms
A simple framework for adding undo/redo support
Bynschan.
A framework for adding undo/redo support to a Windows Forms application is presented.
C#
Windows, .NET (.NET 1.1)
Win32, VS (VS.NET2003), WinForms
Dev
Posted: 9 Jul 2005
Updated: 21 Jul 2005
Views: 24,568
Announcements
Monthly Competition
Search   Articles Authors   Advanced Search
Sitemap
PrintBroken Article?BookmarkDiscussSend to a friend
19 votes for this article.
Popularity: 5.78. Rating: 4.52 out of 5.
Download demo project (VS.NET 2003) - 11.2 Kb
Introduction
A simple framework for adding undo/redo support to a Windows Forms application is presented here. The framework consists of a small collection of classes and interfaces that helps you to manage invocation of undo/redo functionality. Of course, the framework itself does not perform the underlying undo or redo functionality. This is something application-specific that you need to provide as you extend the framework.
The framework
There are three main classes/interfaces that I want to describe. They are coded within the same source file (UndoSupport.cs) and namespace (UndoSupport) in the attached demo project.
UndoCommand: This is an abstract class that represents an undoable or redoable operation or command. It provides virtual Undo() and Redo() methods which your derived command classes can override in order to perform the underlying undo/redo functionality.
public abstract class UndoCommand : IUndoable { // Return a short description of the cmd // that can be used to update the Text // property of an undo or redo menu item. public virtual string GetText() { // Empty string. return ""; } public virtual void Undo() { // Empty implementation. } public virtual void Redo() { // Empty implementation. } }
In a class that inherits from UndoCommand, you also have the option of not overriding the virtual Undo() and Redo() methods. Instead, you can treat the derived command class like a data class and simply provide extra fields, properties, or methods that an external class (one that implements the IUndoHandler interface, as discussed below) can use to perform the actual undo/redo functionality.
IUndoHandler: This is an optional interface that your application classes can implement if you don't want a particular UndoCommand class to perform the underlying undo/redo functionality itself. Use of this interface allows you to keep all of the undo/redo logic within a single class if you like (e.g., the class that implements IUndoHandler), and use the UndoCommand classes only for storing the data (e.g. snapshots of application state) that is needed to perform undo/redo.
public interface IUndoHandler { void Undo(UndoCommand cmd); void Redo(UndoCommand cmd); }
UndoManager: This is the primary class in the framework. As you perform operations in your application, you create command objects and add them to the undo manager. The undo manager handles when to invoke undo/redo functionality for you. When you add a new command, you can optionally specify an IUndoHandler to perform undo/redo of that command. It is possible to mix command objects that can perform undo/redo on their own together with command objects that rely on an IUndoHandler implementation. The UndoManager class is designed to be used directly within undo/redo menu item event handlers and undo/redo menu item state update functions (which makes it easy to implement standard Edit | Undo and Edit | Redo menu item functionality).
public class MyForm : System.Windows.Forms.Form { ... private void OnEditUndoClick(object sender, System.EventArgs e) { // Perform undo. m_undoManager.Undo(); } }
For reference, here is the public interface of the UndoManager class:
Collapse
public class UndoManager { // Constructor which initializes the // manager with up to 8 levels // of undo/redo. public UndoManager() {...} // Property for the maximum undo level. public int MaxUndoLevel {...} // Register a new undo command. Use this method after your // application has performed an operation/command that is // undoable. public void AddUndoCommand(UndoCommand cmd) {...} // Register a new undo command along with an undo handler. The // undo handler is used to perform the actual undo or redo // operation later when requested. public void AddUndoCommand(UndoCommand cmd, IUndoHandler undoHandler) {...} // Clear the internal undo/redo data structures. Use this method // when your application performs an operation that cannot be undone. // For example, when the user "saves" or "commits" all the changes in // the application, or when a form is closed. public void ClearUndoRedo() {...} // Check if there is something to undo. Use this method to decide // whether your application's "Undo" menu item should be enabled // or disabled. public bool CanUndo() {...} // Check if there is something to redo. Use this method to decide // whether your application's "Redo" menu item should be enabled // or disabled. public bool CanRedo() {...} // Perform the undo operation. If an undo handler was specified, it // will be used to perform the actual operation. Otherwise, the // command instance is asked to perform the undo. public void Undo() {...} // Perform the redo operation. If an undo handler was specified, it // will be used to perform the actual operation. Otherwise, the // command instance is asked to perform the redo. public void Redo() {...} // Get the text value of the next undo command. Use this method // to update the Text property of your "Undo" menu item if // desired. For example, the text value for a command might be // "Draw Circle". This allows you to change your menu item Text // property to "&Undo Draw Circle". public string GetUndoText() {...} // Get the text value of the next redo command. Use this method // to update the Text property of your "Redo" menu item if desired. // For example, the text value for a command might be "Draw Line". // This allows you to change your menu item text to "&Redo Draw Line". public string GetRedoText() {...} // Get the next (or newest) undo command. This is like a "Peek" // method. It does not remove the command from the undo list. public UndoCommand GetNextUndoCommand() {...} // Get the next redo command. This is like a "Peek" // method. It does not remove the command from the redo stack. public UndoCommand GetNextRedoCommand() {...} // Retrieve all of the undo commands. Useful for debugging, // to analyze the contents of the undo list. public UndoCommand[] GetUndoCommands() {...} // Retrieve all of the redo commands. Useful for debugging, // to analyze the contents of the redo stack. public UndoCommand[] GetRedoCommands() {...} } The TestUndo application
The demo project (TestUndo) shows the framework in action. It's a simple Windows application with just a single form. All of the undo/redo framework code is in the UndoSupport.cs file as mentioned earlier. All of the application code that uses the framework is contained within the MainForm.cs file. Below is a snapshot of the TestUndo application:

The MainForm is divided into two group box sections. The top section is the Test Area and offers a simple GUI that allows you to perform some undoable operations. These operations consist of appending short strings to a multiline display textbox. There are three buttons that allow you to add a specific string. A different undo command class (derived from UndoCommand) is associated with each button. The first two command classes (AddABCCommand and Add123Command) do not implement their own undo/redo functionality. They rely on an IUndoHandler implementation to perform the actual undo/redo. The IUndoHandler reference must be specified when these types of commands are added to the undo manager.
public class MainForm : System.Windows.Forms.Form, IUndoHandler { ... private void OnAddABC(object sender, System.EventArgs e) { // Create a new command that saves the "current state" // of the display textbox before performing the AddABC // operation. AddABCCommand cmd = new AddABCCommand(m_displayTB.Text); // Perform the AddABC operation. m_displayTB.Text += "ABC "; // Add the new command to the undo manager. We pass in // "this" as the IUndoHandler. m_undoManager.AddUndoCommand(cmd, this); } }
The third command class (AddXYZCommand) does implement its own undo/redo functionality. That's why in its constructor, the display textbox is passed in. The AddXYZCommand class needs to access the display textbox in order to perform undo/redo by itself.
class AddXYZCommand : UndoCommand { private string m_beforeText; private TextBox m_textBox; public AddXYZCommand(string beforeText, TextBox textBox) { m_beforeText = beforeText; m_textBox = textBox; } public override string GetText() { return "Add XYZ"; } public override void Undo() { m_textBox.Text = m_beforeText; } public override void Redo() { m_textBox.Text += "XYZ "; } }
To undo the add operations, the MainForm provides a main menu with Edit | Undo and Edit | Redo menu items. You can use these menu items or their keyboard shortcuts to perform undo/redo of the three types of add operations. The Clear button clears the display textbox and also clears all outstanding undo/redo commands (since I have deemed this particular operation as being not undoable). As you perform add, undo, or redo operations, you can access the Edit menu and see how the undo and redo menu item text changes. For example, instead of simply displaying "Undo", you can see the undo menu item displaying "Undo Add ABC" after you press the Add ABC button.
Collapse
public class MainForm : System.Windows.Forms.Form, IUndoHandler { ... private void OnEditMenuPopup(object sender, System.EventArgs e) { // Update the enabled state of the undo/redo menu items. m_undoMenuItem.Enabled = m_undoManager.CanUndo(); m_redoMenuItem.Enabled = m_undoManager.CanRedo(); // Change the text of the menu items in order // specify what operation to undo or redo. m_undoMenuItem.Text = "&Undo"; if ( m_undoMenuItem.Enabled ) { string undoText = m_undoManager.GetUndoText(); if ( undoText.Length > 0 ) { m_undoMenuItem.Text += " " + undoText; } } m_redoMenuItem.Text = "&Redo"; if ( m_redoMenuItem.Enabled ) { string redoText = m_undoManager.GetRedoText(); if ( redoText.Length > 0 ) { m_redoMenuItem.Text += " " + redoText; } } } }
The bottom section of the MainForm shows you what's happening behind the scenes in the data structures maintained by the UndoManager class. The UndoManager is implemented using an ArrayList to store the history of commands for undo purposes, and a Stack to store commands for redo purposes. You can see command objects being shuffled between the two data structures as you invoke the undo or redo menu items. By default, the UndoManager supports up to eight levels of undo (meaning it can backtrack up to 8 commands). You can use the MainForm GUI to test different values for the maximum undo level (but note that changing the level will cause existing undo/redo commands to be cleared).
That's basically all for the TestUndo application. I wrote it (MainForm.cs) primarily to illustrate how to use the UndoManager class, to exercise all of its public methods, and to provide some confidence to the reader that the internal undo/redo state is being managed properly (according to "standard" undo/redo behavior). My intent was to create a simple application to demonstrate the above, rather than focus on creating a real application with actual graphics, cut and paste, or text editing commands. There are other articles on CodeProject which discuss this in relation to undo/redo and I encourage you to check those out as well.
Summary
The presented framework can be a starting point for adding undo/redo support to your own Windows Forms applications. The most important part about using the framework is figuring out how to partition your undoable user operations into command classes, deciding what extra fields you need to store in those classes, and then figuring out how to actually undo and redo those operations. In simple cases, undo can be implemented by saving the current state of some application variables before you perform the operation (so that when it is time to undo, you just restore that archived state). In more complex situations, such as when your user operations involve database or data structure manipulations, you will need to be able to write routines to reverse (undo) or re-apply (redo) those manipulations. Discussion of how to implement such commands I believe is application-specific and beyond the scope of what I wanted to demonstrate in this article.
History
July 9th, 2005 Initial revision.
July 10th, 2005 Added some clarifications regarding the scope and purpose of the test application, based on initial feedback from Marc Clifton.
July 19th, 2005 Minor updates to code blocks in the article text.
nschan
Clickhere to view nschan's online profile.
Other popular C# Programming articles:
A flexible charting library for .NET Looking for a way to draw 2D line graphs with C#? Here's yet another charting class library with a high degree of configurability, that is also easy to use.
I/O Ports Uncensored - 1 - Controlling LEDs (Light Emiting Diodes) with Parallel Port Controlling LEDs (Light Emiting Diodes) with Parallel Port
Asynchronous Method Invocation How to use .NET to call methods in a non-blocking mode.
I/O Ports Uncensored Part 2 - Controlling LCDs (Liquid Crystal Displays) and VFDs (Vacuum Fluorescent Displays) with Parallel Port Controlling LCDs (Liquid Crystal Displays) and VFDs (Vacuum Fluorescent Displays) with Parallel Port

[Top]Sign in to vote for this article:     PoorExcellent

Note: You mustSign in to post to this message board.
FAQ  Message score threshold 1.0 2.0 3.0 4.0 5.0    Search comments
View Normal (slow) Preview (slow) Message View Topic View Thread View Expanded    Per page 10 25 50 (must logon)
  Msgs 1 to 15 of 15 (Total: 15) (Refresh) First Prev Next
Subject  Author  Date

 Simplify the design  NorfyCH  5:22 27 Jul '05
  The design of the UndoManager can be greatly simplified by refactoring it around the IUndoable interface.
public interface IUndoable
{
String Text { get; } // Use a property instead of GetText()
void Undo();
void Redo();
}
public void Add(IUndoable undoable) {}
// These sort of lines...
if ( info.m_undoHandler != null )
info.m_undoHandler.Undo(info.m_undoCommand);
else
info.m_undoCommand.Undo();
// ...can be replaced by
undoable.Undo()
In your design UndoInfo would implement IUndoable.
Whether one uses a Command or Memento pattern is then irrelevant ie. the UndoManager can handle either.
[Sign in |View Thread |PermaLink |Go to Thread Start] Score: 2.0 (1 vote).

 Re: Simplify the design  nschan  23:16 27 Jul '05
  Thanks for the feedback,
Good point about using Text property instead of GetText(). At first, I did consider making better use of IUndoable, except that I didn't feel the Text property belonged there, thinking it was more command-related. Also, I wanted to have an abstract UndoCommand to allow derived classes to inherit the empty implementations, which are defaults that some commands will likely use (e.g., there are operations for which you don't need any special Text value and it is fine to leave the Undo/Redo menu item text unchanged).
But your redesign does have merit and does simplify things. I wouldn't say it greatly simplifies the UndoManager implementation though, as you've traded one type (UndoCommand) for another (IUndoable) and moved the ("these sorts of lines...") logic from the UndoManager and into the nested UndoInfo class instead. You've increased encapsulation internally but I think the amount of code and basic logic is almost the same. Let me know if I am missing something here.
In general though, your refactoring idea sounds good to me.
regards
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: Simplify the design  NorfyCH  8:06 28 Jul '05
  "Text" is simply a description of the "undoable" action and is required by the UndoManager API so I see no reason why it can't be part of the interface.
You can still have your abstract UndoCommand since it is just one implementation of the IUndoable interface.
nschan wrote:
You've increased encapsulation internally but I think the amount of code and basic logic is almost the same.
Increased encapsulation and decoupled the classes. The UndoManager is now only coupled to one interface not two classes. The amount of code would only be slightly reduced but workflow would be controlled through polymorphism not explicit if..then code blocks.
Using an interface also means someone can use their own classes which are not UndoCommands or involve an IUndoHandler.
OK, maybe "greatly simplifies" was an exaggeration!
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: Simplify the design  nschan  23:18 29 Jul '05
  Sure, all your points make sense to me. And as I mentioned before, I think your redesign looks good. What I like is that you found a use for IUndoableI'd like to try out your suggested changes myself later but haven't had a chance to revisit the code yet.
regards
[Sign in |View Thread |PermaLink |Go to Thread Start]


 Web apps that support undo/redo  uy_do  12:20 21 Jul '05
  Thanks for a great article. I'd like to know how to implement this undo/redo in a web environment.
Any ideas?
Thanks.
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: Web apps that support undo/redo  nschan  0:10 22 Jul '05
  Interesting idea. I've mostly seen undo/redo discussed at the level of controls (e.g., a rich text edit control that supports undo/redo by itself, in a self-contained way). As far as a generic framework for a browser app, I'm guessing some cases could be handled by storing/recalling snapshots of client state (using asp.net state service or database?). But my experience here is limited. I found a link that talks a little bit about this as well:
http://www.dotnet247.com/247reference/msgs/7/37138.aspx[^]
regards
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: Web apps that support undo/redo  uy_do  11:05 25 Jul '05
  [From the link that you gave,] it seems an undo framework for a session-long should be good to go. I dont think storing data on client-side is a good idea, due to the permission limitations a browser has. Best way to me is to store undo commands in temp tables in a DB, to avoid problems when you deal with web-farm servers. I still dont know how to fit your framework into this scenario yet, but hey, it is already there!
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: Web apps that support undo/redo  nschan  20:07 25 Jul '05
  Sounds good. I would agree maintaining just for the session should be sufficient. If you find an approach that works, let us know. I'm sure others would be interested.
regards
[Sign in |View Thread |PermaLink |Go to Thread Start]


 This seems a bit awkward  Marc Clifton  7:54 10 Jul '05
  There are a few things that bother me about this implementation:
First, you have to pass the control in to the undo class. This makes the whole architecture dependent on controls, when undo/redo capability can be applied to really anything, whether it's at the UI level or not.
Also, this doesn't allow the control to be collected when the form goes out of scope? The undo manager is still maintaining references to the undo commands, which in turn maintain references to the controls. I'm not sure this would work in an MDI environment, or for modeless dialogs.
Second, the Redo method adds the same same text that the is being added elsewhere when the button is clicked. This bothers me because you're duplicating the value that's being added to the textbox. In a more complicated scenario, this could get quite awkward, especially when you're not dealing with constants. For example, how would the undo work if you're dealing with a drawing surface and the user wants to redo an arbitrary line he just drew?
Third, you're restoring the control's state. Inmy article[^] on a undo/redo implementation using the IMemento pattern, I drew some criticism because people prefer a command pattern instead, as the state information can get quite large. I'm not sure I agree with the criticism, but it's something to consider.
Lastly, I also don't really like the idea that I would need to implement a separate class for each command. Imagine an application like Visio, or Word, which can have thousands of commands. There might be something to be said for having a class per command, but then, in the OnAddABC event handler, the adding of the text should really be done in your AddABCCommand class. This would help to resolve my concern #2.
I haven't voted on your article, and I look forward to your feedback, as I may have misunderstood some of the internal implementation.
Marc
My website
Latest Articles:
Object Comparer
String Helpers
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: This seems a bit awkward  Paul Selormey  14:54 10 Jul '05
  Marc Clifton wrote:
First, you have to pass the control in to the undo class. This makes the whole architecture dependent on controls, when undo/redo capability can be applied to really anything, whether it's at the UI level or not.
What do you mean by "control"?
Best regards,
Paul.
Jesus Christ is LOVE! Please tell somebody.
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: This seems a bit awkward  Marc Clifton  14:57 10 Jul '05
  Paul Selormey wrote:
What do you mean by "control"?
As in, the System.Windows.Form.Control object:
public AddXYZCommand(string beforeText, TextBox textBox)
AddXYZCommand being derived from UndoCommand.
Marc
My website
Latest Articles:
Object Comparer
String Helpers
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: This seems a bit awkward  Paul Selormey  22:51 10 Jul '05
  But AddXYZCommand was just an implementation, right? The main framework he presented has nothing to do with a control class. Or was the original codes different from the updated one, which I viewed?
Best regards,
Paul.
Jesus Christ is LOVE! Please tell somebody.
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: This seems a bit awkward  Marc Clifton  8:06 11 Jul '05
  Paul Selormey wrote:
But AddXYZCommand was just an implementation, right?
Yes, I didn't realize the example was just that, rather than a "best practice" usage. See the other thread where this is explained more by the author.
Marc
My website
Latest Articles:
Object Comparer
String Helpers
[Sign in |View Thread |PermaLink |Go to Thread Start]

 Re: This seems a bit awkward  nschan  15:10 10 Jul '05
  Thanks for the comments, Marc. Please see below.
> First, you have to pass the control in to the undo class.
> This makes the whole architecture dependent on controls,
> when undo/redo capability can be applied to really anything,
> whether it's at the UI level or not.
I agree, undo/redo can be applied to anything. The test application (MainForm.cs) is not really the reuseable part of what I wanted to present. Reuse I think stops at the UndoManager class. The purpose of the test app is primarily to show how to use the UndoManager, to exercise its public methods, and to show users the internal undo/redo state transitions. Everything in MainForm (including the command classes) I tried to keep as simple as possible, rather than focus on creating a real application with actual undoable graphics commands, text editing commands, etc.
Yes, the AddXYZCommand class does require the control (textbox) that it operates on to be passed in. This is just an example of how you need to pass in something, some data, or references into the command class so that it has the ability to undo itself later when requested. What to pass in, or how to implement underlying undo/redo behavior is something application-specific I think and beyond the scope of what I wanted to address in the article. In the XYZ case, there are probably other ways to implement it, such as turning AddXYZCommand into a nested class of MainForm, and passing in instead a reference to the MainForm.
> Also, this doesn't allow the control to be collected when the form goes out
> of scope? The undo manager is still maintaining references to the undo
> commands, which in turn maintain references to the controls. I'm not sure this
> would work in an MDI environment, or for modeless dialogs.
Good point – I didn’t discuss this use case but I think this should be handled by clearing all undo/redo commands at the appropriate time, using the public UndoManager.ClearUndoRedo() method.
> Second, the Redo method adds the same same text that the is being added
> elsewhere when the button is clicked. This bothers me because you're duplicating
> the value that's being added to the textbox. In a more complicated scenario, this
> could get quite awkward, especially when you're not dealing with constants.
> For example, how would the undo work if you're dealing with a drawing surface
> and the user wants to redo an arbitrary line he just drew?
It’s just a coincidence that the GetText() for the testapp commands is the same as what the commands actually do (add some text). In a real example, there would be no relation, as in the case of your graphics example.
> Third, you're restoring the control's state. In article on a undo/redo
> implementation using the IMemento pattern, I drew some criticism
> because people prefer a command pattern instead, as the state information
> can get quite large. I'm not sure I agree with the criticism, but it's something to
> consider.
Yes, in the test application, all the command classes save the current state in order to implement undo/redo. This is just an example. Sometimes saving state is appropriate, and maybe the only solution. In other cases, such as when you are trying to undo manipulations to a big tree data structure, you are likely better off having your command classes be able to manipulate tree nodes, etc. In any case, I consider all this to be app-specific and outside of what I wanted to cover. It does sound like an interesting topic for future articles (to talk more about how to actually implement underlying undo/redo functionality for common application types).
> Lastly, I also don't really like the idea that I would need to implement a separate
> class for each command. Imagine an application like Visio, or Word, which can
> have thousands of commands. There might be something to be said for having a
> class per command, but then, in the OnAddABC event handler, the adding of the
> text should really be done in your AddABCCommand class. This would help to
> resolve my concern #2.
I agree, creating a different class for each type of undoable operation might be overkill. But nothing prevents you from creating a single command class to cover a range of operations. I think you can do this with any kind of command framework. For example, you could have a DrawGraphicsCommand class that covers multiple operations (DrawLine, DrawCircle, DrawRectangle). You could add an integer or enumerated “type” field to the class to allow you to distinguish between the specific operations. Not very object-oriented but likely more practical for some real situations like the one you mention.
PS. I updated the article text with some of the above clarifications on the scope/purpose of the test app.
regards
[Sign in |View Thread |PermaLink |Go to Thread Start] Score: 2.0 (1 vote).

 Re: This seems a bit awkward  Marc Clifton  15:22 10 Jul '05
  nschan wrote:
PS. I updated the article text with some of the above clarifications on the scope/purpose of the test app.
Great! Also, your comments made a lot of sense. Sometimes it's hard to separate the non-reusable example from the re-usable code. Thanks for pointing that out--I think it'll make for a better article, which is advice I will use myself on future articles.
Marc
My website
Latest Articles:
Object Comparer
String Helpers
[Sign in |View Thread |PermaLink |Go to Thread Start]

Last Visit: 9:36 Tuesday 4th September, 2007 First Prev Next
General comment   News / Info   Question   Answer   Joke / Game   Admin message
Updated: 21 Jul 2005 Article content copyright nschan, 2005
everything else Copyright ©CodeProject, 1999-2007.
Web08 |Advertise on The Code Project |Privacy

The Ultimate Toolbox •ASP Alliance •Developer Fusion •Developersdex •DevGuru •Programmers Heaven •Planet Source Code •Tek-Tips Forums •
Help!
Articles
Message Boards
Lounge
What is 'The Code Project'?
General FAQ
Post a Question
Site Directory
About Us
Latest
Most Popular
Search
Site Directory
Submit an Article
Update an Article
Article Competition
Windows Vista
Visual C++
ATL / WTL / STL
COM
C++/CLI
C#
ASP.NET
VB.NET
Web Development
.NET Framework
Mobile Development
SQL / ADO / ADO.NET
XML / XSL
OS / SysAdmin
Work Issues
Article Requests
Collaboration
General Discussions
Hardware
Algorithms / Math
Design and Architecture
Subtle Bugs
Suggestions
The Soapbox