Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ActionList for .NET 2.0

0.00/5 (No votes)
30 Apr 2006 3  
An implementation of Borland's ActionList.

Sample Image - CradsActions.gif

Introduction

Everyone who has worked with Borland's Delphi knows how powerful Actions are. Actions are used to link together various UI elements, such as buttons, menu items, and toolbar buttons, making them behave consistently: linked items are checked/ unchecked/ enabled/ disabled at the same time, and they share the same Text and eventually Image property and, of course, execute the same code once clicked.

Using the code

This library is based on two main components: the ActionList class and the Action class. ActionList, which implements the IExtenderProvider interface, holds an Action's collection, and provides design time support for associating an Action with a ButtonBase or a ToolStripItem derived control. So, you first drop an ActionList from the Toolbox to your WinForm, then you can edit its Actions collection.

After adding and setting the desired values for the Action's properties, you can link it to a control, and you'll notice how its property values like Text, Image, Tooltip, shortcut key, etc., are replaced with the the connected Action's ones.

Each Action exposes two main events: Execute and Update. The Execute event is raised every time one of the linked controls is clicked, so people should trap this event instead of handling the control's Click directly. The Update event is raised while an application is in the idle state, and it's useful to enable/disable linked controls at runtime (you can use it for other purposes, like setting their CheckState, of course).

How actions work behind the scenes

I'm not going to explain every line of code: if you've got a little experience with the .NET Framework, everything is pretty simple and easy to understand, so I'll focus only on a couple of main topics.

First of all, every Action internally holds a collection of target controls. Once a control is added, its properties are refreshed according to the Action's ones, and its Click and CheckStateChanged events are handled (and, of course, once a target is removed, those handlers are removed too):

internal void InternalAddTarget(Component extendee)
{
    // we first add extendee to targets collection

    targets.Add(extendee);
    // we refresh its properties to Action's ones

    refreshState(extendee);
    // we add some handler to its events

    AddHandler(extendee);
    OnAddingTarget(extendee);
}

Let's first take a look on how the target's property setting is handled: all the work is performed by the private updateProperty method:

private void updateProperty(Component target, string propertyName, object value)
{
    WorkingState = ActionWorkingState.Driving;
    try
    {
        if (ActionList != null)
        {
            if (!SpecialUpdateProperty(target, propertyName, value))
                ActionList.TypesDescription[target.GetType()].SetValue(
                    propertyName, target, value);
        }
    }
    finally
    {
        WorkingState = ActionWorkingState.Listening;
    }            
}

As a first step, it changes the Action's working state to Driving (which causes the target's event handling being temporarily disabled), then it sets the target's property to the right value. The only matter is that it works by Reflection, which can be really slow, so I've built a special class named ActionTargetDescriptionInfo which caches the Types' PropertyInfo to speed up all the work.

About event handling: when a control is added to the target's collection, the Action traps its Click and CheckStateChanged events.

protected virtual void AddHandler(Component extendee)
{
    // Click event's handling, if present

    EventInfo clickEvent = extendee.GetType().GetEvent("Click");
    if (clickEvent != null)
    {
        clickEvent.AddEventHandler(extendee, clickEventHandler);
    }

    // CheckStateChanged event's handling, if present

    EventInfo checkStateChangedEvent = 
              extendee.GetType().GetEvent("CheckStateChanged");
    if (checkStateChangedEvent != null)
    {
        checkStateChangedEvent.AddEventHandler(extendee, 
                         checkStateChangedEventHandler);
    }
}

ClickEventHandler and checkStateChangedEventHandler are pretty simple: the first raises the Execute event, the latter updates every target's checkState property according to the Action's one.

private void handleClick(object sender, EventArgs e)
{
    if (WorkingState == ActionWorkingState.Listening)
    {
        Component target = sender as Component;
        Debug.Assert(target != null, "Target is not a component");
        Debug.Assert(targets.Contains(target), 
              "Target doesn't exist on targets collection");

        DoExecute();
    }
}

private void handleCheckStateChanged(object sender, EventArgs e)
{
    if (WorkingState == ActionWorkingState.Listening)
    {
        Component target = sender as Component;
        CheckState = (CheckState)ActionList.
            TypesDescription[sender.GetType()].GetValue("CheckState", sender);
            
    }
}

The last point to explain is how the Update event rising works: it is driven by the ActionList, which handles the Application.Idle and raises this event for every owned Action:

void Application_Idle(object sender, EventArgs e)
{
    OnUpdate(EventArgs.Empty);
}

public event EventHandler Update;
protected virtual void OnUpdate(EventArgs eventArgs)
{
    // we first raise ActionList's Update event

    if (Update != null)
        Update(this, eventArgs);

    // next, we raise child actions update

    foreach (Action action in actions)
    {
        action.DoUpdate();
    }
}

How to create your own custom action

By version 1.1.1.0, Crad's Actions library offers a better support to expandability. It's pretty easy to build your own custom Action: all you have to do is to create a new class which inherits from Crad.Windows.Forms.Actions.Action and mark it with the StandardAction attribute. Designer support is provided by a new implementation of the internal ActionCollectionEditor class, which is able to inspect the current project references, looking for custom Actions.

This behaviour is achieved by the ITypeDiscoveryService designer service, which makes simple solving a really hard assembly-inspecting problem: looking for types at design time can be a rather complex task, indeed, because the current project could not have been built yet, and the corresponding assembly could not exist at all. Using the ITypeDiscoveryService, everything becomes easier, so, right now, ActionCollectionEditor has a private method that does the job; it looks like the following code snippet:

private Type[] getReturnedTypes(IServiceProvider provider)
{
    List<Type> res = new List<Type>();

    ITypeDiscoveryService tds = (ITypeDiscoveryService)
        provider.GetService(typeof(ITypeDiscoveryService));
    
    if (tds != null)
        foreach (Type actionType in tds.GetTypes(typeof(Action), false))
        {
            if (actionType.GetCustomAttributes(typeof(
                StandardActionAttribute), false).Length > 0 &&
            !res.Contains(actionType))
                res.Add(actionType);
        }

    return res.ToArray();
}

First of all, it recovers a reference to the designer's ITypeDiscoveryService, then it calls its GetTypes method to recover every code-reachable type that inherits from Action and has been marked with the StandardAction attribute.

Points of interest

Actions can help a Windows Forms developer to coordinate the behaviour of various UI elements in a pretty simple and efficient way. I wanted them be linkable to a lot of .NET Framework 2.0 WinForms controls (they work with every ButtonBase or ToolStripItem derived control), so the only way to handle this requirement was using Reflection. However, the performance drop is compensated using a PropertyInfo caching system, which is able to reuse the metadata information for an object of the same type.

Crad's Actions library ships with some purpose-specific actions too: for example, some of them are helpful to handle clipboard-related operations, such as cut, copy, or paste, while others provide formatting features when applied to a RichTextBox. According to Borland's naming, they're called Standard Actions, and I'm still working on them, so... expect some more for the next releases.

To better understand how easy it is to implement complex user interfaces using Actions, you can check out the simple RTF Editor provided as a demo application.

History

  • 04/30/2006: Crad's Actions 1.1.1.0 released.
    • Support for creating custom Actions added (with a brief description in this article).
    • Some new StandardActions added, like ListView actions and About action.
  • 04/22/2006: First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here