WEBSITE

Application Patterns and Tips : Remember the Screen Location & Implement Undo Using Command Objects

1/12/2012 4:02:52 PM

Remember the Screen Location

Problem:You want your application to remember its location on the screen and restore to that location the next time the app runs.
Solution:Although this task is easy, you need to take into account that when you restore an application, what used to be on the screen before might not be on the screen anymore. For example, a user might rearrange a multiple-monitor scenario, or merely change the resolution of his screen to something smaller.

The screen location should be a user-specific setting. For the following example, two user settings were created in the standard Settings.settings file.

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
RestoreLocation();
}

private void RestoreLocation()
{
Point location = Properties.Settings.Default.FormLocation;
Size size = Properties.Settings.Default.FormSize;
//make sure location is on a monitor
bool isOnScreen = false;
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.Contains(location))
{
isOnScreen = true;
}
}
//if our window isn't visible, put it on primary monitor
if (!isOnScreen)
{
this.SetDesktopLocation(
Screen.PrimaryScreen.WorkingArea.Left,
Screen.PrimaryScreen.WorkingArea.Top);
}

//if too small, just reset to default
if (size.Width < 10 || size.Height < 10)
{
Size = new Size(300, 300);
}
}

private void SaveLocation()
{
//these are user settings I created in the
//Properties\Settings.settings file
Properties.Settings.Default.FormLocation = this.Location;
Properties.Settings.Default.FormSize = this.Size;
Properties.Settings.Default.Save();
}

protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);

SaveLocation();
}
}


Implement Undo Using Command Objects

Problem:You want to be able to undo commands in your application.
Solution:Most programs that let the user edit content have the ability to let the user undo the previous action. This section demonstrates a simple widget application that allows undo functionality (see Figure 1).
Figure 1. This simple application allows the user to undo moves, creations, and deletions.


The most popular way to implement this involves command objects that know how to undo themselves. Every possible action in the program is represented by a command object.

Note

Not everything the user can do in your application needs to be a command. For example, moving the cursor and changing the current selection aren’t usually considered actions. Generally, undoable commands should be those that change the user’s data.


Define the Command Interface and History Buffer

Here’s a possible interface:

interface ICommand
{
void Execute();
void Undo();
string Name { get; }
}

We also need a way to track all our commands in the order they were issued:

class CommandHistory
{
private Stack<ICommand> _stack = new Stack<ICommand>();

public bool CanUndo
{
get
{
return _stack.Count > 0;
}
}

public string MostRecentCommandName
{
get
{
if (CanUndo)
{
ICommand cmd = _stack.Peek();
return cmd.Name;
}
return string.Empty;
}
}

public void PushCommand(ICommand command)
{
_stack.Push(command);
}

public ICommand PopCommand()
{
return _stack.Pop();
}
}


Given these two things, the specific implementations of commands depends on the data structures of the application.

In this case, we have an IWidget interface defining all our objects:

interface IWidget
{
void Draw(Graphics graphics);
bool HitTest(Point point);
Point Location { get; set; }
Size Size { get; set; }
Rectangle BoundingBox { get; }
}

Define Command Functionality

One command we need is to be able to undo a drag/move operation. The command object needs only as much context to be able to do and undo the operation (in this case, the old location and the new location):

class MoveCommand : ICommand
{
private Point _originalLocation;
private Point _newLocation;
private IWidget _widget;

public MoveCommand(IWidget widget,
Point originalLocation,
Point newLocation)
{
this._widget = widget;
this._originalLocation = originalLocation;
this._newLocation = newLocation;
}

#region ICommand Members

public void Execute()
{
_widget.Location = _newLocation;
}

public void Undo()
{
_widget.Location = _originalLocation;
}

public string Name
{
get { return "Move widget"; }
}
#endregion
}


Here’s the CreateWidgetCommand object, which takes a different type of state:

class CreateWidgetCommand : ICommand
{
private ICollection<IWidget> _collection;
private IWidget _newWidget;

public CreateWidgetCommand(ICollection<IWidget> collection, IWidget newWidget)
{
_collection = collection;
_newWidget = newWidget;
}

#region ICommand Members

public void Execute()
{
_collection.Add(_newWidget);
}

public void Undo()
{
_collection.Remove(_newWidget);
}

public string Name
{
get { return "Create new widget"; }
}

#endregion
}


To use this functionality, you just have to create the command objects at the appropriate time. Here is the Form from the CommandUndo sample code. Look at the project in Visual Studio to see the full source.

public partial class Form1 : Form
{
private CommandHistory _history = new CommandHistory();
private List<IWidget> _widgets = new List<IWidget>();
private bool _isDragging = false;
private IWidget _dragWidget = null;
private Point _prevMousePt;
private Point _originalLocation;
private Point _newLocation;
public Form1()
{
InitializeComponent();

panelSurface.MouseDoubleClick += new MouseEventHandler(panelSurface_MouseDoubleClick);
panelSurface.Paint += new PaintEventHandler(panelSurface_Paint);
panelSurface.MouseMove +=
new MouseEventHandler(panelSurface_MouseMove);
panelSurface.MouseDown +=
new MouseEventHandler(panelSurface_MouseDown);
panelSurface.MouseUp +=
new MouseEventHandler(panelSurface_MouseUp);

editToolStripMenuItem.DropDownOpening += new EventHandler(editToolStripMenuItem_DropDownOpening);
undoToolStripMenuItem.Click +=
new EventHandler(undoToolStripMenuItem_Click);
}

void panelSurface_MouseDown(object sender, MouseEventArgs e)
{
IWidget widget = GetWidgetUnderPoint(e.Location);
if (widget != null)
{
_dragWidget = widget;
_isDragging = true;
_prevMousePt = e.Location;
_newLocation = _originalLocation = _dragWidget.Location;
}
}

void panelSurface_MouseMove(object sender, MouseEventArgs e)
{
if (!_isDragging)
{
IWidget widget = GetWidgetUnderPoint(e.Location);
if (widget != null)
{
panelSurface.Cursor = Cursors.SizeAll;
}
else
{
panelSurface.Cursor = Cursors.Default;
}
}
else if (_dragWidget != null)
{
Point offset = new Point(e.Location.X - _prevMousePt.X,
e.Location.Y - _prevMousePt.Y);

_prevMousePt = e.Location;

_newLocation.Offset(offset);
//update the widget temporarily as we move
//-- not a command in this case
//because we don't want to record every dragging operation
_dragWidget.Location = _newLocation;

Refresh();
}
}

void panelSurface_MouseUp(object sender, MouseEventArgs e)
{
if (_isDragging)
{
//now perform the command so that Undo restores to location
//before we started dragging
RunCommand(new MoveCommand(_dragWidget,
_originalLocation,
_newLocation));
}
_isDragging = false;
_dragWidget = null;
}

void panelSurface_MouseDoubleClick(object sender, MouseEventArgs e)
{
CreateNewWidget(e.Location);
}

private IWidget GetWidgetUnderPoint(Point point)
{
foreach (IWidget widget in _widgets)
{
if (widget.BoundingBox.Contains(point))
{
return widget;
}
}
return null;
}

void panelSurface_Paint(object sender, PaintEventArgs e)
{
foreach (IWidget widget in _widgets)
{
widget.Draw(e.Graphics);
}
}

//menu handling
void editToolStripMenuItem_DropDownOpening(object sender,
EventArgs e)
{
undoToolStripMenuItem.Enabled = _history.CanUndo;
if (_history.CanUndo)
{
undoToolStripMenuItem.Text = "&Undo "
+ _history.MostRecentCommandName;
}
else
{
undoToolStripMenuItem.Text = "&Undo";
}
}

void undoToolStripMenuItem_Click(object sender, EventArgs e)
{
UndoMostRecentCommand();
}

private void createToolStripMenuItem_Click(object sender,
EventArgs e)
{
CreateNewWidget(new Point(0, 0));
}

private void clearToolStripMenuItem_Click(object sender,
EventArgs e)
{
RunCommand(new DeleteAllWidgetsCommand(_widgets));
Refresh();
}
private void CreateNewWidget(Point point)
{
RunCommand(new CreateWidgetCommand(_widgets,
new Widget(point)));
Refresh();
}

private void RunCommand(ICommand command)
{
_history.PushCommand(command);
command.Execute();
}

private void UndoMostRecentCommand()
{
ICommand command = _history.PopCommand();
command.Undo();
Refresh();
}
}


Note

Although WPF has the notion of command objects already, they do not have the ability to undo themselves (which makes sense because undo is an application-dependent operation). The ideas in this section can be easily translated to WPF.

Other  
  •  Application Patterns and Tips : Use an Event Broker
  •  AJAX : Updating Progress
  •  AJAX : The Timer
  •  Getting Familiar with AJAX
  •  ASP.NET Server-Side Support for AJAX & AJAX Client Support
  •  ASP.NET and AJAX
  •  IIS 7.0 : Securing Communications with Secure Socket Layer (SSL)
  •  ASP.NET 4 : Getting More Advanced with the Entity Framework (part 2) - Updates, Inserts, and Deletes
  •  ASP.NET 4 : Getting More Advanced with the Entity Framework (part 1) - Querying with LINQ to Entities
  •  IIS 7.0 : Implementing Access Control - Authentication (part 4)
  •  IIS 7.0 : Implementing Access Control - Authentication (part 3) - IIS Client Certificate Mapping Authentication
  •  IIS 7.0 : Implementing Access Control - Authentication (part 2) - Digest Authentication & Windows Authentication
  •  IIS 7.0 : Implementing Access Control - Authentication (part 1)
  •  IIS 7.0 : Implementing Access Control - NTFS ACL-based Authorization & URL Authorization
  •  IIS 7.0 : Implementing Access Control - Request Filtering
  •  IIS 7.0 : Implementing Access Control - IP and Domain Restrictions
  •  IIS 7.0 : Implementing Security Strategies - Configuring Applications for Least Privilege
  •  Security Changes in IIS 7.0 : Reducing the Application’s Surface Area
  •  Advanced ASP.NET : The Entity Framework (part 3) - Handling Errors & Navigating Relationships
  •  Advanced ASP.NET : The Entity Framework (part 2)
  •  
    Top 10
    Beginning Android 3 : Working with Containers - Scrollwork
    DirectX 10 Game Programming : Direct3D Fonts
    Customizing Windows 7’s Desktop (part 3) - Getting Around the Taskbar
    Publishing ASP.NET Web Applications : MSDeploy Publish
    Parallel Programming : Task Relationships (part 2) - Parent and Child Tasks
    Secure Browsing and Local Machine Lockdown in Vista
    SQL Server 2005 Data Protection
    SharePoint 2010 : Identifying Isolation Approaches to SharePoint Security
    Programming Microsoft SQL Server 2005 : FOR XML Commands (part 3) - OPENXML Enhancements in SQL Server 2005
    Windows 7 : Installing and Configuring Windows Media Center Using the Wizard
    Most View
    Optimizing for Vertical Search : Optimizing for Image Search (part 2) - Optimizing Through Flickr and Other Image Sharing Sites
    Exchange Server 2010 : Installing OCS 2007 R2 (part 1) - Extending the Active Directory (AD) Schema & Preparing the AD Forest
    Get to a SharePoint Site
    Mouse Events in Silverlight
    Programming the Mobile Web : Content Delivery (part 3)
    # Oracle Coherence 3.5 : Achieving Performance, Scalability, and Availability Objectives (part 2)
    Programming Hashing Algorithms (part 4) - Hashing Streamed Data
    Programmatic Security (part 6) - Assembly-Wide Permissions
    The Hello-World Midlet
    Silverlight Recipes : Updating the UI from a Background Thread
    Windows 7: Getting into Your Multimedia (part 1) - Configuring Windows Media Player for the First Use
    Windows Phone 7 Development : Media - Adding Sounds to an Application
    Windows Phone 7 Development : Push Notifications - Implementing Raw Notifications
    ActiveX Installer Service in Windows Vista
    Windows Mobile Security - Local Data Storage
    iPhone Application Development : Creating and Managing Image Animations and Sliders (part 3) - Finishing the Interface
    Communicate over the Internet (WCF)
    Performing a typical Exchange Server 2010 install
    Sharepoint 2007: Add a Column to a List or Document Library
    Application Security in Windows Vista