Click here to Skip to main content
15,881,381 members
Articles / Programming Languages / C#
Article

ActionList for .NET 2.0

Rate me:
Please Sign up or sign in to vote.
4.89/5 (52 votes)
30 Apr 20065 min read 187.7K   2.5K   122   82
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.

Image 2

Image 3

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.

Image 4

Image 5

Image 6

Image 7

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):

C#
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:

C#
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.

C#
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.

C#
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:

C#
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:

C#
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


Written By
Web Developer
Italy Italy
Marco De Sanctis is an italian developer and a software consultant. His interests are focused on object-oriented architecture and data-driven applications.

Comments and Discussions

 
QuestionCan't Add new actions to actionlist Pin
Wayne Sawyer10-Aug-06 2:13
Wayne Sawyer10-Aug-06 2:13 
AnswerRe: Can't Add new actions to actionlist Pin
RicMan27-Dec-06 10:36
RicMan27-Dec-06 10:36 
AnswerRe: Can't Add new actions to actionlist Pin
Shuriken8717-Aug-07 2:35
Shuriken8717-Aug-07 2:35 
QuestionAction names Pin
bjp7774-Aug-06 0:31
bjp7774-Aug-06 0:31 
AnswerRe: Action names Pin
Cradle774-Aug-06 10:11
Cradle774-Aug-06 10:11 
AnswerRe: Action names Pin
bjp7775-Aug-06 0:12
bjp7775-Aug-06 0:12 
GeneralDoes not work for UserControls Pin
Karel Kral18-Jul-06 3:01
Karel Kral18-Jul-06 3:01 
AnswerRe: Does not work for UserControls Pin
Cradle774-Aug-06 10:02
Cradle774-Aug-06 10:02 
Known bug, it will be fixed on the next release
GeneralRe: Does not work for UserControls Pin
John Gunnarsson2-Oct-07 0:37
John Gunnarsson2-Oct-07 0:37 
GeneralRe: Does not work for UserControls Pin
John Gunnarsson2-Oct-07 0:42
John Gunnarsson2-Oct-07 0:42 
GeneralFeature request: remove bound targets Pin
Igor Velikorossov12-Jul-06 15:33
Igor Velikorossov12-Jul-06 15:33 
GeneralFeature request #2 Pin
Igor Velikorossov12-Jul-06 15:54
Igor Velikorossov12-Jul-06 15:54 
JokeExtend supported types Pin
Igor Velikorossov11-Jul-06 20:42
Igor Velikorossov11-Jul-06 20:42 
GeneralRe: Extend supported types Pin
Cradle7711-Jul-06 21:26
Cradle7711-Jul-06 21:26 
GeneralRe: Extend supported types Pin
Igor Velikorossov11-Jul-06 21:29
Igor Velikorossov11-Jul-06 21:29 
QuestionRe: Extend supported types Pin
Uwe Keim20-Aug-06 6:44
sitebuilderUwe Keim20-Aug-06 6:44 
QuestionImageList property? Pin
Koru.nl26-May-06 7:52
Koru.nl26-May-06 7:52 
AnswerRe: ImageList property? Pin
Cradle7726-May-06 10:26
Cradle7726-May-06 10:26 
GeneralRe: ImageList property? Pin
Koru.nl26-May-06 10:42
Koru.nl26-May-06 10:42 
GeneralNullReferenceException in ActionList.ContainerControl Pin
Igor Velikorossov21-May-06 17:30
Igor Velikorossov21-May-06 17:30 
GeneralRe: NullReferenceException in ActionList.ContainerControl [modified] Pin
Cradle7722-May-06 2:16
Cradle7722-May-06 2:16 
GeneralRe: NullReferenceException in ActionList.ContainerControl [modified] Pin
Igor Velikorossov22-May-06 13:02
Igor Velikorossov22-May-06 13:02 
GeneralRe: NullReferenceException in ActionList.ContainerControl [modified] Pin
Cradle7722-May-06 13:09
Cradle7722-May-06 13:09 
GeneralI think Youd could add Document Suport to it Pin
bidanjun8-May-06 9:53
bidanjun8-May-06 9:53 
GeneralRe: I think Youd could add Document Suport to it Pin
Cradle778-May-06 12:11
Cradle778-May-06 12:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.