65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 2.0 的 ActionList

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (50投票s)

2006年4月22日

5分钟阅读

viewsIcon

195968

downloadIcon

2463

Borland 的 ActionList 的实现。

Sample Image - CradsActions.gif

引言

所有接触过 Borland Delphi 的人,都知道 Actions 的强大之处。Actions 用于连接各种 UI 元素,例如按钮、菜单项和工具栏按钮,使它们表现一致:链接的项会同时被选中/取消选中/启用/禁用,它们共享相同的 Text 和可能的 Image 属性,当然,点击时执行相同的代码。

使用代码

这个库基于两个主要组件:ActionList 类和 Action 类。ActionList 实现 IExtenderProvider 接口,包含一个 Actions 集合,并提供设计时支持,用于将 Action 与 ButtonBaseToolStripItem 派生控件关联。因此,您首先将一个 ActionList 从工具箱拖放到您的 WinForm 上,然后就可以编辑它的 Actions 集合了。

添加并设置 Action 属性的所需值后,您可以将其链接到一个控件,您会注意到它的 TextImageTooltip、快捷键等属性值会被替换为连接的 Action 的值。

每个 Action 都暴露两个主要事件:ExecuteUpdate。每当链接的控件被点击时,都会引发 Execute 事件,所以人们应该捕获这个事件,而不是直接处理控件的 Click 事件。Update 事件在应用程序处于空闲状态时引发,用于在运行时启用/禁用链接的控件(当然,您也可以将其用于其他目的,例如设置它们的 CheckState)。

Actions 在后台如何工作

我不会解释每一行代码:如果您对 .NET Framework 有一些经验,一切都会很简单易懂,所以我将只关注几个主要话题。

首先,每个 Action 内部都包含一个目标控件的集合。一旦添加了一个控件,它的属性就会根据 Action 的属性进行刷新,并且它的 ClickCheckStateChanged 事件会被处理(当然,一旦移除了一个目标,那些事件处理器也会被移除)。

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);
}

让我们先来看看目标属性的设置是如何处理的:所有工作都由私有的 updateProperty 方法完成。

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;
    }            
}

作为第一步,它将 Action 的工作状态更改为 Driving(这会导致目标控件的事件处理被暂时禁用),然后将目标控件的属性设置为正确的值。唯一的问题是它通过反射工作,这可能非常慢,所以我构建了一个名为 ActionTargetDescriptionInfo 的特殊类,它缓存了类型的 PropertyInfo 以加快所有工作速度。

关于事件处理:当一个控件被添加到目标控件集合中时,Action 会捕获它的 ClickCheckStateChanged 事件。

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);
    }
}

ClickEventHandlercheckStateChangedEventHandler 非常简单:第一个引发 Execute 事件,第二个根据 Action 的属性更新每个目标的 checkState 属性。

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);
            
    }
}

最后一点要解释的是 Update 事件是如何引发的:它由 ActionList 驱动,ActionList 处理 Application.Idle 事件并为每个拥有的 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();
    }
}

如何创建您自己的自定义 Action

从 1.1.1.0 版本开始,Crad's Actions 库提供了更好的可扩展性支持。创建您自己的自定义 Action 非常简单:您只需创建一个继承自 Crad.Windows.Forms.Actions.Action 并用 StandardAction 属性标记它的新类。设计器支持由内部 ActionCollectionEditor 类的新实现提供,该类能够检查当前项目的引用,查找自定义 Actions。

这种行为是通过 ITypeDiscoveryService 设计器服务实现的,它使得解决一个非常棘手的程序集检查问题变得简单:在设计时查找类型可能是一项相当复杂的任务,因为当前项目可能尚未构建,并且相应的程序集可能根本不存在。使用 ITypeDiscoveryService,一切都变得更容易,因此,现在 ActionCollectionEditor 有一个私有方法来完成这项工作;它看起来像下面的代码片段。

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();
}

首先,它恢复对设计器的 ITypeDiscoveryService 的引用,然后调用其 GetTypes 方法来恢复所有可代码访问的、继承自 Action 并且被标记为 StandardAction 的类型。

关注点

Actions 可以帮助 Windows Forms 开发人员以一种非常简单高效的方式协调各种 UI 元素的行为。我希望它们能够链接到许多 .NET Framework 2.0 WinForms 控件(它们适用于所有 ButtonBaseToolStripItem 派生控件),因此处理此要求的唯一方法是使用反射。但是,性能下降通过使用 PropertyInfo 缓存系统得到了补偿,该系统能够重用相同类型对象的元数据信息。

Crad's Actions 库还附带了一些特定目的的 Actions:例如,其中一些有助于处理剪贴板相关操作,如剪切、复制或粘贴,而另一些则在应用于 RichTextBox 时提供格式化功能。根据 Borland 的命名,它们被称为 Standard Actions,我还在开发它们,所以……期待在未来的版本中会有更多。

要更好地理解使用 Actions 实现复杂用户界面的便捷性,您可以查看作为演示应用程序提供的简单 RTF 编辑器。

历史

  • 2006/04/30: Crad's Actions 1.1.1.0 发布。
    • 增加了对创建自定义 Actions 的支持(本文有简要说明)。
    • 增加了一些新的 StandardActions,例如 ListView actions 和 About action。
  • 2006/04/22: 第一个版本。
© . All rights reserved.