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)
{
targets.Add(extendee);
refreshState(extendee);
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)
{
EventInfo clickEvent = extendee.GetType().GetEvent("Click");
if (clickEvent != null)
{
clickEvent.AddEventHandler(extendee, clickEventHandler);
}
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)
{
if (Update != null)
Update(this, eventArgs);
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
StandardAction
s added, like ListView
actions and About
action.
- 04/22/2006: First version.