[翻译]使用设计模式简化.NET中菜单和Form元素之间的关系

  利用.NET Windows Form 写程序菜单时,感觉代码写的很丑陋,微软没有提供优雅的解决方案吗?上网搜了一下,发现了这篇文章 翻译过来,中文标题是<<使用设计模式简化.NET中菜单和Form元素之间的关系>>。匆匆浏览了一下,没看懂。里面主要用到了设计模式里的命令模式(Command Pattern),就先去看了看Headfirst 设计模式里介绍Command Pattern的这一章,挺简单的,看明白了。然后花了整整一天时间才把上面的文章看明白了。搜了一下,没有相应的中文,而且之前也没有搜到相应的中文资料,所以把此文章翻译了过来。文中对应的代码下载在这里

  本文很长,我觉得学知识应该知道这个知识的前世今生,知其然,知其所以然。所以没有进行精简,浓缩。有些句子跟本文主题关系不大,略去不翻(真实原因是我的英文水平有限)。请像我这样的菜鸟们花整段的时间,比如一上午来理解这篇文章,因为里面的知识点还是比较多的。如果现在没有时间看,而您又想把它看明白,可以把它放到TO-DO list里,挑个合适的时间看。
  如果对设计模式中的命令模式不熟悉,可以先去看一下园子里这篇,介绍挺详细的。

----------------------------------------正文-----------------------------------

  总结:
    在windows Forms程序中,相似的命令,例如菜单上的命令和在工具栏上与之对应的命令,并没有自动关联。他们没有触发相同的事件或者运行相同的句柄(handler)处理函数。所以允许同一个或者相同的用户命令触发相同的代码会简化开发。
    
    这篇文章描述了命令管理时的原则和使用并不针对特定UI元素的命令的重要性。为了设计出一个MFC式的命令管理架构,使用了一个设计模式并用一些C#类实现了它。

 内容
    .Net中的事件处理
    命令的逻辑概念
    命令管理者
    命令状态
    实现.NET下的命令管理者
    命令执行者
    命令管理者内部实现
    命令内部实现
    命令执行者的实现
    在空闲时进行命令更新
    例子程序
    结论

  命令管理因为有许多很有用的应用而成为应用程序框架中的一个受欢迎的功能。例如,命令管理让你在一个函数中处理类似的菜单和工具栏事件。而且,处理命令的业务逻辑也被集中到一个地方,而不是散布在许多函数中。命令管理的关键在于命令管理器,它是一个程序中所有可用命令的集中仓库。命令管理者的职责是给应用程序提供一个维护命令列表,执行命令对应的操作,响应命令事件的机制。应用程序可以动态添加删除命令,也可以遍历可用的命令集合。遍历命令的功能让应用程序可以实现菜单或者工具栏的动态配置。

  MFC程序中实现了命令管理的很多特性。允许每个命令和一个OnUpdate函数联系在一起,这样既可以在一个集中的地方根据应用程序中其他对象的状态来设置命令的状态(例如enabled, disabled or checkd)。

  在Windows@ Forms中,微软引进了一个用于开发基于Windows应用程序的新框架。它提供了和菜单与工具栏交互的机制,也提供了一个以事件为中心的通知系统。但不像MFC,Windows Forms没有提供集中的命令管理。菜单项和工具栏上对应的条目并没有相关性,他们并不会触发相同的事件。

  本文中,先讨论典型的命令管理系统的功能,然后简单介绍一下MFC如何提供这些功能。之后讨论如何处理.NET中的命令和随之带来的问题。最终,我们实现了一个.NET命令管理器,它提供了一个命令管理系统的常见的功能。

 .NET中事件处理

  在传统的基于Windows-MFC的程序中,命令是由命令ID来代表的。尽管命令ID不完全是请求自身的一种面向对象的封装,但它至少让程序员能够在UI元素和对应的动作之间建立一个逻辑关系。而且MFC提供了一个UI更新的句柄,可以支持在程序空闲时控制命令的状态。  

  .NET对象通过事件来发送通知,而不是通过消息。

  .NET事件与传统的基于消息的Windows机制相比有一些优点:
    首先,它是一种更面向对象的方法,任意的对象都可以订阅事件。
    其次,.NET事件允许多播。这意味着多个对象可以订阅同一个通知,当事件发生时,所有的订阅者就会得到通知,而这些订阅者之间不需要互相了解。

  不幸的是,尽管有这个很好的面向对象的架构,但Windos Forms中并没有把命令当作对象这个概念。而且,为了把开发者和底层windows基于消息的机制隔离开来,之前提到的命令ID的概念也不存在了。

  下面一节我们将举例说明为什么这种功能是可取的,然后提供一个.NET下命令管理的实现。 

  命令的逻辑概念

   把命令封装成对象的想法并不是新的。在这方面设计模式(Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides)一书中有着精彩的讨论。

  一个命令代表一个请求的逻辑封装。在大多数情况下,这些请求由应用程序中的用户界面,如菜单项,工具栏中的按钮来触发,但他们也可以由宏或者是外部资源来执行,甚至以编程方式执行。

  那你为什么想把应用程序的命令本身作为单独的对象封装起来呢?这种方法最主要的优点是把请求和触发这个请求的UI对象分离开来了。

  在一个程序中,一个命令可以有多个物理的表示形式。例如,多数面向文档的程序提供一个保存操作,来让用户把文档保存到文件中。一个菜单项,一个工具栏上的按钮,或者是一个键盘快捷键都可以用来表示它 。没有保存命令这个概念,这些UI元素之间就是完全相互独立的;他们触发各自的通知并拥有各自的状态。   

  把一个命令实体和它在应用程序UI中的视觉实体分离出来的想法可以让你的每个逻辑操作有一个统一的处理过程并对命令状态进行集中式管理。

  让我们继续以保存操作作为例子。没有命令对象,那你就不得不订阅菜单项对象的单击事件并订阅工具栏上的按钮的按钮单击事件,然后在两个地方都添加对应的代码。尽管可以让两个事件处理的委托都指向一个共同的执行实际操作的函数,但这样确保两个事件的响应中执行了相同的操作就变成了程序员的责任。框架自己却没有强制做到这个。例如,可以想象到会有这种情况,UI界面中两个对应到保存操作的元素会触发两个完全不同的操作。这很大情况下是个bug而不是预期的功能,但程序员需要保证手动提供了一个共同的处理操作时,十有八九会发生这种情况。

  一个命令也可能有一个取决于程序的上下文环境的状态。让我们看看多数程序中都有的拷贝操作。常见情况是只有当应用程序中有选中的项目时,拷贝操作才可用。

  假设应用程序包含一个Listbox控件。当Listbox拥有焦点并有一个有效的条目被选中时,允许拷贝操作。当ListBox失去焦点或者不包含一个有效的选项时,应该不允许用户执行拷贝操作。这意味着所有的可以触发拷贝操作的UI对象应该是在disabled状态。应用程序可能在listbox的选中状态改变时有一个事件处理,当listbox失去焦点时有另一个事件处理。如果不把命令和UI元素分离开来,两个事件中不得不像下面这样处理:

Toolbar.Buttons("Copy").Enabled = false
mnuCopy.Enabled = false

  这样,listbox的事件处理部分就必须知道应用程序中跟listbox自身状态有关的所有操作同时也得知道所有代表这些操作的UI元素。在一个命令对象中封装了实际的请求后,开发人员可以这样写代码:   

CommandManager.Command("Copy").Enabled = false

  这会导致自动更新Copy命令对应的所有的物理表示形式。一个命令管理的框架应该提供这样一种机制:在一个地方更新所有命令的表示形式的状态。一个禁止一个命令的请求应该可以自动更新所有的和这个命令关联的用户界面元素。写代码禁止一个菜单项或者一个工具栏项是相当枯燥的。

  想象一下如果你允许用户自定义工具栏和菜单的布局。在这种情景下,针对一个命令的所有UI表示形式的存在与否在设计时并不知道。因此,没有简单的办法来给每个按钮写出单独的事件处理函数,也不能枚举所有的UI界面元素来单独修改他们的状态。

  命令管理者

  正如前面我们提到的,命令管理者是程序中所有可用命令的一个中央存储库。一旦命令添加到命令管理者中,程序需要把这个命令和对应的UI元素关联起来。在拷贝例子中提到的,一个命令在除了传统的菜单项和工具栏按钮外,可以有多个UI表示。我们提出的设计的目标之一是开放,这样第三方控件就可以和命令关联到一起。这些对象应该同传统的菜单和工具栏对象一样可以很简单的和命令管理者整合到一起。命令管理者负责维护命令和其对应UI表示之间的关联。

  传统的响应命令的方式是给每个UI元素写一个事件处理过程。如果一个工具栏按钮和一个菜单项都代表一个命令,那么需要两个不同的事件处理过程。每个事件处理可以委托给一个共同的函数来执行所需的业务逻辑。有了命令管理的架构,所有的事件处理过程都和命令相关联,而不是和UI元素相关联。一个处理过程就可以控制所有的UI表示。如果程序中添加了一个命令的新的UI表示,那么这个命令相关的事件处理中的业务逻辑不需要任何修改。  

  尽管命令通常是通过标准的UI控件来触发的,但也可能期望编程触发命令来响应一系列没有被菜单或者工具栏表示的事件。命令管理者应该允许通过API来执行一个命令,从而允许执行所有已注册的事件处理程序。

  命令状态

  一个命令的状态通常是enabled, disabled, 和 checked. 拷贝例子中提到了,命令管理者应该提供了一个更新命令状态的机制,这样命令的新状态就可以传递给此命令的所有UI表示了。            

   程序负责管理命令的状态。命令对象应该有可以让程序设置命令的状态的函数接口。命令对应的UI应该自动的显示命令的状态。

  之前的例子中,当listbox有焦点并被选中了一个有效的项目时,拷贝命令就被允许了。当Listbox改变它的焦点状态或者选择的状态时,程序需要调用命令管理者来更新拷贝命令的状态。这样问题就出现了:定义了命令的状态的业务逻辑分散在各个地方。下图1展示了这种情况下管理命令状态的伪码。

  图1

void OnListboxSelectionChanged() 
{
    UpdateCopyCommand();
}

void OnListboxFocusChanged() 
{
    UpdateCopyCommand();
}

void UpdateCopyCommand() 
{
    bool bListboxFocus = listbox.HasFocus();
    bool bValidSelection = (listbox.GetSelection() != -1);
    CommandManager.Commands("Copy").Enable = bListbox && bValidSelection;
}

  Copy命令的状态背后的业务逻辑封装在了UpdateCopyCommand中。但是,程序仍然需要知道所有可能影响拷贝命令状态的地方并且确保在每个地方都调用了同一个Update函数。

  命令管理者应该允许在空闲处理时更新命令。在此时,命令管理者遍历所有的命令并触发命令对应的Update事件。这是MFC采取的方法。有了这种方法,一个命令状态后面的逻辑就被封装到一个单独的地方,这样就不依赖于在每个可能影响命令状态的应用程序事件中去调用命令的状态处理函数了。

  实现一个.NET命令管理者

  下面2展示了命令管理者实现时涉及的对象和他们之间的关系。

  图二

  我们已经说过命令管理者(command manager)和命令(command)对象的概念了。在实现这些对象之前,我们首先介绍命令执行者(command executor)的概念。

  命令执行者

  之前说过,命令管理器架构的目标之一就是应该可以和任何可能被用来表示命令的UI元素协同工作。本文例子仅限于菜单项和工具栏按钮,因为他们是十分常见的并且是标准Windows Forms命名空间的一部分。但是,应该允许在命令管理器架构中加入任何第三方控件。

    为了实现这个目标,命令对象自身并不知道在UI上用来表示这个命令的类型。如果这样做了就失去了这种可扩展性,因为每个控件可能声明了一个不同的接口来和事件通信,同时也可能用不同的属性来表示命令的状态。 

  命令执行者这个类充当了命令实例(command instances)和对应的UI表示之间的桥梁。正如图2所示,命令执行者是一个抽象的基类,它提供了命令操作控件状态所需的接口。在窗口中,可以充当命令请求来源的每个类型需要继承CommandExecutor类。这篇文章可下载的代码包括两个实现:MenuCommandExecutor和ToolbarCommandExecutor。

  CommandExecutor的职责包括:
      1.维护对应类型的所有UI实例及其对应命令之间的联系
      2.订阅UI对象产生的事件,并把它们反馈给命令对象。
      3.封装了特定的接口来控制对应的UI对象的状态。

  命令管理器内部细节

  现在来看看实现。在图3中可以看到命令管理器类的实现代码。首先,命令管理器维护两个内部集合:commands和hashCommandExecutors.
        图3:  CommandManager.cs的代码片段

using System;
using System.Collections;
using System.Timers;
using System.Windows.Forms;

namespace CommandManagement{
public class CommandManager : System.ComponentModel.Component{
    // Member Variables
    private CommandsList        commands;
    private Hashtable           hashCommandExecutors;   

    // Constructor
    public CommandManager(){
        commands                = new CommandManager.CommandsList(this);
        hashCommandExecutors    = new Hashtable();

        // Setup idle processing
        Application.Idle += new EventHandler(this.OnIdle);

        // By default, menus and toolbars are known
        RegisterCommandExecutor(    "System.Windows.Forms.MenuItem", 
                                    new MenuCommandExecutor());

        RegisterCommandExecutor(    "System.Windows.Forms.ToolBarButton", 
                                    new ToolbarCommandExecutor());}

    // Commands Property: Fetches the Command collection
    public CommandsList Commands{
        get{
            return commands;
        }
    }

    // Command Executor association methods
    internal void RegisterCommandExecutor(  string          strType, 
                                            CommandExecutor executor){
        hashCommandExecutors.Add(strType, executor);}

    internal CommandExecutor GetCommandExecutor(object instance){
        return hashCommandExecutors[instance.GetType().ToString()] 
            as CommandExecutor;}

    //  Handler for the Idle application event.
    private void OnIdle(object sender, System.EventArgs args){
        IDictionaryEnumerator myEnumerator = 
            (IDictionaryEnumerator)commands.GetEnumerator();
        while ( myEnumerator.MoveNext() ){
            Command cmd = myEnumerator.Value as Command;
            if (cmd != null)
                cmd.ProcessUpdates();}
    }
}   // end class CommandManager

}  // end namespace

  commands是程序中所有可用的命令的集合。每个命令有一个唯一的用户指定的标签(user-specified tag)来把它和其他的命令区分开来。CommandsList对象是一个类型安全的集合,实现了ICollection和IEnumerable接口。 这个集合可以枚举命令或者通过唯一的tag来直接索引。CommandsList类的实现是从CommandManager代码中剥离出来的,因为它是一个集合模板。在下载的代码中可以看到完整的代码。  

  hashCommandExecutors是一个存储程序中所有可用CommandExecutor的哈希表。记住每个可以代表一个命令的UI类型只有一个CommandExecutor。CommandExecutor是一个连接命令对象和它们的物理表示的胶水。

  命令管理器的构造函数设置了默认的功能。除了创建内部数据结构,它还执行了另外两个功能。首先,创建一个程序空闲处理周期的事件处理函数。  

Application.Idle += new EventHandler(this.OnIdle);

   其次,针对MenuItems和ToolBarButton对象分别创建了对应的CommandExecutor。命令管理器在内部拥有对这两个传统的UI对象的支持。程序完全可以实现附加的CommandExecutors。在本文后面会有CommandExecutors的代码。

  RegisterCommandExecutor函数把一个CommandsExecutor和一个Windows控件类型联系在一起。    

RegisterCommandExecutor( "System.Windows.Forms.MenuItem", 
                        new MenuCommandExecutor() );

RegisterCommandExecutor( "System.Windows.Forms.ToolBarButton", 
                        new ToolbarCommandExecutor());

     CommandManager在hashCommandExecutors哈希表中维护CommandsExecutor和对应的Windows控件类型联系,代码如下所示:       

internal void RegisterCommandExecutor(string strType, 
         CommandExecutor executor)
{
   hashCommandExecutors.Add(strType, executor);
}

internal CommandExecutor GetCommandExecutor(object instance)
{
   return hashCommandExecutors[instance.GetType().ToString()] 
         as CommandExecutor;
}

  这里,GetCommandExecutor函数用来获得和一个指定的UI控件关联的CommandExecutor。

 命令内部实现

  Command对象代码见图4。Command对象构造函数初始化了对象的内部状态。command对象的唯一标识是strTag。handlerExecute和handlerUpdate分别用来处理OnExecute和OnUpdate事件。附加的处理可以用标准的事件订阅语法来指定。当空闲处理周期时,会触发OnUpdate事件,让程序有机会更新命令的状态。当一个命令被执行时,OnExecute事件会被触发。

  图4

using System;
using System.Collections;
using System.Timers;
using System.Windows.Forms;

namespace CommandManagement{
public class Command{
    // Members
    private CommandInstanceList commandInstances;
    private CommandManager      manager;
    private string              strTag;
    protected bool              enabled;
    protected bool              check;

    // Constructor
    public Command( string          strTag, 
                    ExecuteHandler  handlerExecute, 
                    UpdateHandler   handlerUpdate){
        commandInstances = new CommandInstanceList(this);
        this.strTag = strTag;
        OnUpdate += handlerUpdate;
        OnExecute += handlerExecute;}

    // CommandInstances collection
    public CommandInstanceList CommandInstances{
        get {
            return commandInstances;
        }
    }

    // Tag: Unique internal name for each command
    public string Tag
    {
        get {return strTag; }
    }

    // Manager property: maintain association with parent command manager
    internal CommandManager Manager{
        get {
            return manager;}
        set{
            manager = value;
        }
    }

    public override string ToString(){
        return Tag;}

    // Methods to trigger events
    public void Execute(){
        if (OnExecute != null)
            OnExecute(this);}

    internal void ProcessUpdates(){
        if (OnUpdate != null)
            OnUpdate(this);}

    // Enabled property
    public bool Enabled
    {
        get{
            return enabled;}
        set{
            enabled = value;
            foreach(object instance in commandInstances){
                Manager.GetCommandExecutor(instance).Enable(
                    instance, enabled);}
        }
    }

    // Checked property
    public bool Checked{
        get{
            return check;}
        set{
            check = value;
            foreach(object instance in commandInstances){
                Manager.GetCommandExecutor(instance).Check(
                    instance, check);}
        }
    }

    // Events
    public delegate void UpdateHandler(Command cmd);
    public event UpdateHandler OnUpdate;

    public delegate void ExecuteHandler(Command cmd);
    public event ExecuteHandler OnExecute;
}
}

  事件定义如下:

public class Command
{
   public delegate void UpdateHandler(Command cmd);
   public event UpdateHandler OnUpdate;

   public delegate void ExecuteHandler(Command cmd);
   public event ExecuteHandler OnExecute;
}

  在构造函数中,可以看到commandInstances这个集合被初始化。这个集合维护了命令和命令对应的所有UI表示之间的关系。通过Add函数,可以一次加一个或者一组UI元素到这个集合中(见图5 )。这些函数中的item参数是代表命令的实际UI元素,通常是一个MenuItem或者是ToolbarButton对象。完整的代码见可下载的代码包。 

public class CommandInstanceList : System.Collections.CollectionBase
{
   public void Add(object instance)
   {
      this.List.Add(instance);
   }

   public void Add(object[] items)
   {
      foreach (object item in items)
      {
         this.Add(item);
      }
   }
 }

  command对象暴露出Enabled和Checked属性来控制命令的状态。这些属性的实现见图4,但仔细看一下Command对象的Enabled属性的Set方法:    

public bool Enabled
{
   set
   {
      enabled = value;
      foreach(object instance in commandInstances)
      {
         Manager.GetCommandExecutor(instance).Enable(
         instance, enabled);
      }
   }
}

  这段代码循环遍历命令关联的每个UI元素。对于每个UI元素,我们查询和它关联的CommandExecutor。然后调用CommandExecutor对应的Enabled函数,这样CommandExecutor就可以执行改变特定UI控件状态的逻辑。Checked的代码也是类似的。

  命令执行者的实现

  CommandExecutor基类定义了Enable和Check虚函数,这样每个派生类就必须实现它,提供特定UI的逻辑。CommandExecutor类的代码见图6. CommandExecutor也包含一些内部的功能。使用一个哈希表来维护某个特定类型的所有的UI元素和UI元素对应的命令对象之间的映射关系。

  图6

using System;
using System.Collections;
using System.Timers;
using System.Windows.Forms;

namespace CommandManagement{
// Command Executor base class
public abstract class CommandExecutor{
    protected Hashtable hashInstances = new Hashtable();

    public virtual void InstanceAdded(object item, Command cmd){
        hashInstances.Add(item, cmd);}

    protected Command GetCommandForInstance(object item){
        return hashInstances[item] as Command;}

       // Interface for derived classed to implement
    public abstract void Enable(object item, bool bEnable);
    public abstract void Check(object item, bool bCheck);}
}

  当通过Command的Add函数把命令对应的UI元素添加到命令的实例列表中时,会调用界面元素对应的CommandExecutor的InstanceAdded函数。CommandInstanceList集合包含了如下的代码来把命令对象和UI元素联系到一起。  

public class CommandInstanceList : System.Collections.CollectionBase
{
   protected override void OnInsertComplete(
      System.Int32 index, System.Object value)
   {
      command.Manager.GetCommandExecutor(value).InstanceAdded(value, 
        command);
   }
}

  CollectionBase对象在一个元素加入到集合后,会调用OnInsertComplete函数。可以看到,先得到UI元素对应的CommandExecutor,然后调用了CommandExecutor的InstanceAdded函数。

  图7包含了Menu和Toolbar CommandExecutors的实现。它们的实现是很简单明了的。MenuCommandExecutor实现了Enable和Check这两个虚函数。UI对象被强制转换成MenuItem对象,这时就可以设置MenuItem的 Enabled和Checked属性了。ToolbarCommandExecutor代码类似:

public override void Enable(object item, bool bEnable)
{
   MenuItem mi = (MenuItem)item;
   mi.Enabled = bEnable;
}

public override void Check(object item, bool bCheck)
{
   MenuItem mi = (MenuItem)item;
   mi.Checked = bCheck;
}

  图7

using System;
using System.Collections;
using System.Timers;
using System.Windows.Forms;

namespace CommandManagement{
// Menu command executor
public class MenuCommandExecutor : CommandExecutor{
    public override void InstanceAdded(object item, Command cmd){
        MenuItem mi = (MenuItem)item;
        mi.Click += new System.EventHandler(menuItem_Click);

        base.InstanceAdded(item, cmd);}

    // State setters
    public override void Enable(object item, bool bEnable){
        MenuItem mi = (MenuItem)item;
        mi.Enabled = bEnable;}

    public override void Check(object item, bool bCheck){
        MenuItem mi = (MenuItem)item;
        mi.Checked = bCheck;}

    // Execution event handler
    private void menuItem_Click(object sender, System.EventArgs e){
        Command cmd = GetCommandForInstance(sender);
        cmd.Execute();
    }
}

// Toolbar command executor
public class ToolbarCommandExecutor : CommandExecutor{
    public override void InstanceAdded(object item, Command cmd){
        ToolBarButton button = (ToolBarButton)item;
        ToolBarButtonClickEventHandler handler = 
            new ToolBarButtonClickEventHandler(toolbar_ButtonClick);

        // Attempt to remove the handler first, in case we have already 
        // signed up for the event in this toolbar
        button.Parent.ButtonClick -= handler;
        button.Parent.ButtonClick += handler;

        base.InstanceAdded(item, cmd);}

    // State setters
    public override void Enable(object item, bool bEnable){
        ToolBarButton button = (ToolBarButton)item;
        button.Enabled = bEnable;}

    public override void Check(object item, bool bCheck){
        ToolBarButton button = (ToolBarButton)item;
        button.Style = ToolBarButtonStyle.ToggleButton;
        button.Pushed = bCheck;}

    // Execution event handler
    private void toolbar_ButtonClick(   object                      sender, 
                                        ToolBarButtonClickEventArgs args){
        Command cmd = GetCommandForInstance(args.Button);
        cmd.Execute();
    }

}

}

  MenuCommandExecutor也重载了InstanceAdded函数并订阅了MenuItem的单击事件。

public override void InstanceAdded(object item, Command cmd)
{
   MenuItem mi = (MenuItem)item;
   mi.Click += new System.EventHandler(menuItem_Click);

   base.InstanceAdded(item, cmd);
}

  当单击了某个菜单项,就会触发菜单项的单击事件,这样menuItem_Click事件就被调用了。 MenuCommandExecutor找到跟这个菜单项关联的命令,然后调用这个命令的Execute方法。这样就调用了跟命令关联的OnExecute事件的处理函数。  

private void menuItem_Click(object sender, System.EventArgs e)
{
   Command cmd = GetCommandForInstance(sender);
   cmd.Execute();
}

   在空闲时进行命令的更新

  我们已经看到了Check和Enable属性如何控制命令的状态,但还有一块知识没有涉及到。之前提到了,我们期望把命令的状态封装起来,这样程序就不用在所有可能影响命令状态的地方调用命令的状态处理逻辑了。早先我们看到了CommandManager实现了程序空闲事件的处理,现在让我们仔细看看这个处理过程:  

private void OnIdle(object sender, System.EventArgs args)
{
   IDictionaryEnumerator myEnumerator =  
      IDictionaryEnumerator)commands.GetEnumerator();
   while ( myEnumerator.MoveNext() )
   {
      Command cmd = myEnumerator.Value as Command;
      if (cmd != null)
         cmd.ProcessUpdates();
   }
}

  当执行空闲处理时,命令管理器遍历命令,在每个命令上触发其Update事件,这样应用程序就可以设置命令的状态了。      

  例子程序

  为了说明这些类的用法,让我们看看一个使用了命令管理器特性的例子程序。  

  这个例子在一个窗口上使用了RichEdit控件。有五个命令可用,可以从菜单项或者工具栏的按钮来执行他们。图8 展示了这个编辑器例子。

  图8

      这个编辑器首先要做的就是创建初始的命令并把它们和对应的UI关联在一起。下面的例子代码创建了一个Copy命令并把它和对应的一个菜单项和一个工具栏按钮关联在一起。

private void InitializeCommandManager()
{
   cmdMgr = new CommandManager();

   cmdMgr.Commands.Add( new Command(
      "EditCopy", 
      new Command.ExecuteHandler(OnCopy), 
      new Command.UpdateHandler(UpdateCopyCommand)));

   cmdMgr.Commands["EditCopy"].CommandInstances.Add(
      new Object[]{mnuEditCopy, tlbMain.Buttons[4]});
}

  拷贝命令解释了在空闲处理中触发Update事件的好处。拷贝命令的状态取决于edit控件中是否有选中的文本。拷贝命令的OnUpdate事件处理过程的代码如下:

public void UpdateCopyCommand(Command cmd)
{
   cmd.Enabled = txtEditor.SelectedText.Length > 0;
}

  根据文本编辑器中的选中状态,一行代码就可以允许或者禁止Edit|Copy 菜单项和工具栏上的Copy按钮。

   Bold命令也说明了幕后进行更新的特性。当执行这个命令后,会针对编辑器中选中的字体使用粗体。通过这个命令的Checked状态来把这个属性的值展示给用户。

  public void UpdateBoldCommand(Command cmd)
  {
     cmd.Checked = txtEditor.SelectionFont.Bold;
  }

  这个程序中定义的其他命令是Open,Save和Print。这些命令没有实际的功能,但他们共同的OnExecute处理函数说明了不论是哪种类型的请求,都被重定向到相同的处理函数中。

  结论

  命令管理器可以帮助我们把程序的业务逻辑和它的用户界面分离开。这样会产生干净,可维护的代码,同时也使在这个架构上添加更高级的特性成为可能。

    

  

posted on 2012-12-06 10:47  生栋  阅读(1559)  评论(2编辑  收藏  举报

导航