C# Corner

Combine Generics and Functional Programming

Mixing generics and functional programming simplifies writing some extensibility libraries tremendously. For example, combining these techniques makes it easy to create a generic Undo library.

TECHNOLOGY TOOLBOX: C#

It's often simple to build standalone new functionality. The .NET Framework and the C# language provide a rich set of tools that make it easy to build the functionality you need. It can be much more difficult to design something new that fits into an existing application. Often, you'll pull out your full set of possible solutions and pick the one that feels most natural. Without thinking, you've made it more difficult for many of your users to work with the new design by implicitly pushing a set of constraints on those users -- constraints they might not be able to live by without extensive re-work.

A little more thought can improve life for your users considerably. As more of your work becomes additions to existing applications, you need to adjust your thinking to ensure that you create components that are easy to incorporate into existing applications.

Recently I needed to add Undo capability to an existing application. I reviewed many existing frameworks and other articles that make it easier to support Undo in a new application before adopting the approach described in this article, which combines generics and functional programming. Any of the existing frameworks would have been fine if I were starting a new development effort. However, I needed to add the same capability to an existing project, without major changes to all the existing code. That required a different approach.

At its core, adding an Undo feature means that you have to define two methods for every command, one that executes the command and another that can undo whatever that command did. Once you have that code handy, it's not that hard to manipulate the sequence of commands to perform an Undo or Redo from any location.

Pass Methods as Data
Keeping that code handy means passing methods as data. To create an Undo command, you need to define a method that does something, as well as a corresponding method to undo that same something. Then, you'll start manipulating those delegates and invoking them to undo and redo actions.

Begin by building this from the inside out. The first type you must build is a class that stores the Redo action and the Undo action:

private class UndoRedoActions
{
    
	public Action RedoAction
    {
        get;
        private set;
    }
    public Action UndoAction
    {
        get;
        private set;
    }

    public UndoRedoActions(Action cmd, 
        Action undoCmd)
    {
        RedoAction = cmd;
        UndoAction = undoCmd;
    }
}

The Action delegate is defined in the core .NET Framework, and it represents a method that takes no parameters and has a void return-type.

The UndoRedoActions class provides a type to store those two delegates as data and manipulate them. You need to construct your object with two different Actions, and the internal library can handle both the Undo and Redo actions. Next, you must store a list of these objects to facilitate multiple Undo and Redo actions (see Listing 1).

The UndoHandler class manages the Undo and Redo stacks and provides the functionality for a multilevel Undo and Redo. If you're writing an application, you call the ExecuteCommand method when you want to execute a command that can be undone. You check the CanUndo and CanRedo properties to enable or disable your application's Undo and Redo commands. Also, you can call the Undo and Redo methods to undo a command or redo that command.

What's important here is not that you've created an Undo class, but that you've done so with the absolute minimum requirements on any application that wishes to use that class. To use this class, an application developer must first write code that executes a command, and then write code that can undo a command. You don't have any demands on how those methods must be implemented. There's no requirement to create an interface, no requirement to derive from a specific base class. There are no demands, period; you just need to write the code you need for the specific commands.

Use the Library
You've built the library; the next step is to put it to work.

I made a small sample using Conway's Game of Life, which is a cell-automation simulation. I chose it because it was a small demo, yet it does enough that the Undo capabilities are interesting.

Adding Undo to the simulation requires just a few steps. First, create an UndoHandler as a member of the main form:

UndoHandler cmdManager = new UndoHandler();

Beyond that, you need to use the UndoHandler whenever you execute a command. For example, in this simulation, you can press and drag the mouse button over the grid to add new live cells. To undo those changes, you can simply replace the new grid with the old grid. Here's the code that specifies how the action is performed and undone:

protected override void 
OnMouseMove(MouseEventArgs e)
{
    if ((e.Button & MouseButtons.Left) == 0)
        return;

    int i = grid.GridSize * e.X / ClientSize.Width;
    int j = grid.GridSize * e.Y / ClientSize.Height;

    LifeGrid oldGrid = grid;

    cmdManager.ExecuteCommand(() =>
        grid = grid.QueueUserItem(i,j),
        () => grid = oldGrid);
    grid = grid.QueueUserItem(i, j);
}

C# 3.0 (with Generics and lambda expressions) makes writing this kind of code amazingly easy. You can call ExecuteCommand() easily using Lambda syntax, which in turn makes it easy to pass the method definitions into the Undo Handler. The lambda syntax also creates a closure, which enables you to allow the command to access local variables; learn more about this technique in Kathleen Dollard's article, "Capture Variables with Closures" (Ask Kathleen, February 2008). Doing this makes it easy to undo the command by replacing the updated grid with the original grid.

You can follow the same pattern with all the other commands, including the New command:

private void newToolStripMenuItem_Click(
    object sender, EventArgs e)
{
    LifeGrid oldGrid = grid;
    cmdManager.ExecuteCommand(() =>
        {
            grid = grid.seedGrid();
            updateImage();
        },
        
		() =>
        
		{
            grid = oldGrid;
            updateImage();
        }
    );
}

This code illustrates how lambdas can contain multiple statements in C#. Whatever code you would write to perform an action, or back it out, can be accommodated.

What's left is to add the actual support for Undo and Redo. I took a bit of a shortcut with this step. Rather than add event handlers when the Undo and Redo UI elements were drawn, I chose to check the state of the cmdManager when a user picked Undo or Redo:

private void 
    undoToolStripMenuItem_Click(
    object sender, EventArgs e)
{
    if (cmdManager.CanUndo)
        cmdManager.Undo();
}
private void 
    redoToolStripMenuItem_Click(
    object sender, EventArgs e)
{
    if (cmdManager.CanRedo)
        cmdManager.Redo();
}

The cmdManager object moves commands between the Undo and Redo stacks. Be sure to download the sample code for this article and examine how the cmdManager does its work for other commands (see Go Online).

The key to this solution and the UndoHandler class is that there are no constraints placed on the design or architecture for your application. Somewhere, you'll have code that executes commands.

When you build components, be sure to spend some time thinking about how easy (or difficult) you make it for other developers to use what you're building. Pay special attention to the developers that are building the nth release of a product. That can lead to extra work for them if you've introduced stronger coupling between your library and their code.

Additionally, be sure to look for APIs based on lambda expressions or delegates; such APIs make it much easier for them -- and you.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube