2008-12-05 54 views
4

“顶级”类将一个事件附加到一个可能在callstack中“5+层以下”的类的最佳设计决策是什么?附加到C#中的调用堆栈中的事件的最佳方法?

例如,也许MainForm产生了一个对象,并且该对象产生了其他几个对象调用的调用堆栈。最明显的方法是将事件链接到对象层次结构,但这看起来很混乱,需要很多工作。

另一个解决方案是使用观察者模式,通过创建一个可公开访问的静态对象来暴露事件,并充当底层对象和顶层“表单”之间的代理。

有什么建议吗?

这是一个伪代码示例。在这个例子中,MainForm实例化'SomeObject',并附加到一个事件。 'SomeObject'附加到它实例化的对象上,以便将事件传递给MainForm侦听器。

class Mainform 
{ 
    public void OnLoad() 
    { 
     SomeObject someObject = new SomeObject(); 
     someObject.OnSomeEvent += MyHandler; 
     someObject.DoStuff(); 
    } 

    public void MyHandler() 
    { 
    } 
} 



class SomeObject 
{ 
    public void DoStuff() 
    { 
     SomeOtherObject otherObject = new SomeOtherObject(); 
     otherObject.OnSomeEvent += MyHandler; 
     otherObject.DoStuff(); 
    } 


    public void MyHandler() 
    { 
     if(OnSomeEvent != null) 
      OnSomeEvent(); 
    } 


    public event Action OnSomeEvent; 
} 

回答

4

如果您的应用程序不是基于复合UI应用程序块,最简单的解决方案是在Main窗体和您的其他组件之间放置一个“侦听器”类,这两个类都可以轻松访问。从概念上讲,类布局如下:

 
    ----------   ---------------- 
    | MainForm |  | Some Component | 
     ---------   ---------------- 
      |     | 
     Hooks onto   Notifies 
      |     | 
      \    /
      ----------------- 
      | Proxy Notifier | 
      ----------------- 

下面是一些示例代码:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      FakeMainForm form = new FakeMainForm(); 
      form.CreateComponentAndListenForMessage(); 
      Console.ReadKey(true); 
     } 
    } 

    class FakeMainForm 
    { 
     public FakeMainForm() 
     { 
      Listener.AddListener(MessageRecieved); 
     } 

     void MessageRecieved(string msg) 
     { 
      Console.WriteLine("FakeMainForm.MessageRecieved: {0}", msg); 
     } 

     public void CreateComponentAndListenForMessage() 
     { 
      ComponentClass component = new ComponentClass(); 
      component.PretendToProcessData(); 
     } 
    } 

    class Listener 
    { 
     private static event Action<string> Notify; 

     public static void AddListener(Action<string> handler) 
     { 
      Notify += handler; 
     } 

     public static void InvokeListener(string msg) 
     { 
      if (Notify != null) { Notify(msg); } 
     } 
    } 

    class ComponentClass 
    { 
     public void PretendToProcessData() 
     { 
      Listener.InvokeListener("ComponentClass.PretendToProcessData() was called"); 
     } 
    } 
} 

这个程序的输出如下:

FakeMainForm.MessageRecieved: ComponentClass.PretendToProcessData() was called

此代码,您可以直接调用方法对任何听众来说,无论他们在调用堆栈中有多远。

它很容易重写你的Listener类,使它更通用一些,适用于不同类型,但你应该明白。

+0

很好的答案!谢谢。 – user43823 2008-12-06 20:14:57

0

我最初的意图是试图避免这种情况,以便一个对象的范围有明显的界限。在表格的特殊情况下,我会尝试让孩子的父母表单管理与其祖先进行的所有必需的通信。你能更具体地了解你的情况吗?

0

我的第一个想法是,从你的MainForm的角度来看,它应该不知道5层下来会发生什么。它应该只知道它与它产生的对象的相互作用。因此,如果主窗体想要异步执行某些操作,则应该可以通过异步调用生成的对象上的方法来实现该功能。如果你允许调用者异步执行某种方法,则不需要再向下推事件模型......只需将方法直接调用到堆栈中即可。你已经在另一个线程上。

希望这会有所帮助。只要记住你的应用程序的级别应该只知道它们下面的级别发生了什么。

+0

我不希望我的MainForm'异步执行动作',我希望它附加到它实例化的对象的更新,而不需要它实例化的对象不得不形成对象层次结构'事件链'。 – user43823 2008-12-05 22:50:06

0

WPF使用routed events。这些是静态的,可以冒泡或向下挖掘元素树。我不知道你是否使用WPF,但静态事件的想法可能会帮助你。

0

我不会说这是一个设计错误,主表单需要倾听某个对象正在做什么的正确理由。我遇到的一种情况是向用户显示状态消息,以指示后台进程在做什么,或者多线程应用程序中有多个控件正在做什么,这样可以让多个屏幕/“页面”一次打开。

在复合用户界面应用程序块中,当其实例化对象在同一工作项目(工作项目只是一组相关用户控件的对象容器)中时,依赖项注入容器的基本等价物将事件连接起来。它通过扫描特殊属性来完成此操作,如事件[EventPublication("StatusChanged")]和公共方法[EventSubscription("StatusChanged")]。我的一个应用程序使用此功能,以便在应用程序内部实例化的用户控件可以广播状态信息(例如“加载客户数据... 45%”),而不知道该数据最终会以主窗体的状态栏。

所以一个用户控件可以做这样的事情:


public void DoSomethingInTheBackground() 
{ 
    using (StatusNotification sn = new StatusNotification(this.WorkItem)) 
    { 
     sn.Message("Loading customer data...", 33); 
     // Block while loading the customer data.... 
     sn.Message("Loading order history...", 66); 
     // Block while loading the order history... 
     sn.Message("Done!", 100); 
    } 
} 

...其中StatusNotification类有一个event与像


[EventPublication("StatusChanged")] 
public event EventHandler<StatusEventArgs> StatusChanged; 

的签名......和上述Message()Dispose()该类上的方法适当地调用该事件。但是这个班并没有明确地把这个事件与任何事情联系起来。对象实例化器将自动将事件连接到具有相同名称的订阅属性的任何人。

所以MainForm中有一个事件处理程序,它看起来是这样的:


[EventSubscription("StatusChanged", ThreadOption=ThreadOption.UserInterface)] 
public void OnStatusChanged(object sender, StatusEventArgs e) 
{ 
    this.statusLabel.Text = e.Text; 
    if (e.ProgressPercentage != -1) 
    { 
     this.progressBar.Visible = true; 
     this.progressBar.Value = e.ProgressPercentage; 
    } 
} 

...或一些这样的。它比这更复杂,因为多个用户控件可以在同一时间广播状态消息,因此它将通过多个状态通知轮换给定的秒数。所以要重新创建这种行为,而不实际切换到CAB(说实话,这比我想的真的要复杂得多),你可以有一个MessageNotificationService对象,你可以通过你的应用程序或你变成一个静态/单一对象(我通常会避免这种方法,因为它很难测试),或者你可以让你的子控件由一个工厂类实例化,这个工厂类可以为你的事件接线。对象可以通过自己创建的属性向工厂注册,也可以通过显式调用“嘿,任何时候您创建带有此签名事件的对象,我想知道它”的方法来注册。

只要注意让任何你实现的类在一个对象被释放的时候解开事件,因为在这种情况下它很愚蠢,最终得到的东西不会被垃圾收集。

希望这会有所帮助!

+0

您有一个优雅的IoC解决方案。也许比我期望的更高级一个答案,但一个很酷的想法。感谢您的贡献! =) – user43823 2008-12-06 20:14:23