记录事件的调用顺序

时间:2013-07-15 16:36:37

标签: .net event-handling

调用特定事件的处理程序的顺序取决于该特定事件的实现。例如,使用多案例委托的默认后备存储,将按照它们注册的顺序调用处理程序。但是类设计者/实现者可能使用addremove关键字来提供具有不同后备存储的事件访问器,因此调用顺序也会不同。

.NET框架基础库本身是否存在事件文档精确描述其调用顺序的任何情况?是否存在,依赖于这样的文件化顺序(例如我自己实施和记录的事件)是否被认为是可接受的做法?为什么或为什么不呢?

3 个答案:

答案 0 :(得分:4)

不是我知道的。这几乎总是先到先出,因为不使用列表效率不高。 MulticastDelegate和EventHandlerList以这种方式工作。

依赖订单是有风险的。有很多情况下,程序员取消订阅事件以防止重入问题,在方法退出时再次订阅它。由于订单将发生不可避免的副作用,他的事件处理程序现在将被调用。如果这导致程序失败,那么该程序员将会困惑很长一段时间。他只是不会看到代码更改和错误行为之间的联系,这是一个非常难以修复的错误。尽管如此,让代码以意想不到的方式进行交互仍然是一个陷阱,只有好的设计才能避免它,只有好的调试器才能对其进行诊断。

答案 1 :(得分:1)

我发布这个问题的那天,我专注于一个特定的案例,其中注册顺序是明显和稳定的。但是第二天我已经注意到这是例外而不是规则。

可以通过任何代码路径在任何地方添加处理程序,因此通常无法对该顺序执行任何有用的操作。因此,一般规则是忽略注册顺序,并尝试使代码工作,无论添加/调用处理程序的顺序如何。这有时需要在您使用代码后采取预防措施。在我目前关注的情况下,这需要创建一个伴随Changing事件,与Changed事件配对,这样必须首先发生的事情将进入Changing事件的处理程序。

我认为您可以记录事件的顺序调用顺序,以查看注册明显且稳定的罕见情况。但是,对于每个依赖于顺序的注册,您还需要记录它的顺序重要性,当您的代码发展时,您必须记住这些重要性,否则会出现问题。听起来很多工作,所以坚持上段提到的一般规则可能会更容易!

我可以想到一种可靠地控制调用顺序的可行方法。您可以将优先级作为处理程序注册的一部分传递。这样,您,具体取决于顺序注册顺序。但是 控制相对调用顺序。但是,这样的实现更重且非标准,因此在大多数情况下可能并不理想。

答案 2 :(得分:0)

我刚刚遇到一个我认为在编写测试时完全可以接受的场景。考虑这个辅助类来异步加载某些东西并在操作完成时通过事件通知:

public class AsyncItemLoader<T>
{
    /// <summary>
    /// Handlers will be invoked in registration order, pinky-swear!
    /// Also, they are invoked on a thread from the ThreadPool.
    /// </summary>
    public EventHandler<T> ItemLoaded;

    public void LoadAsync()
    {
        // load the item asynchronously using Task.ContinueWith to fire 
        // ItemLoaded event to indicate that the item is now available
    }
}

现在假设我们在某个模型类中使用这个帮助器:

public class MyAppModel
{
    private readonly AsyncItemLoader<User> userLoader;

    public AsyncItemLoader<User> User { get { return userLoader; } }

    public MyAppModel()
    {
        this.userLoader = new AsyncItemLoader<User>(...);
        this.userLoader += HandleUserLoaded;
    }

    public void StartLoadingUser()
    {
        userLoader.LoadAsync();
    }

    private void HandleUserLoaded(object sender, User user)
    {
        // do something with the user here
    }
}

好的,现在我们要为MyAppModel编写一个测试,它依赖于HandleUserLoaded在调用StartLoadingUser后的某个时间点所做的事情,比如检查对模拟依赖项的某些方法的调用。

在检查我们的模拟方法的调用之前,我们如何确定地等待HandleUserLoaded完成处理? 很容易,因为事件被记录为按注册顺序调用处理程序,我们知道MyAppModel在客户端有机会注册它们之前注册自己的处理程序:

public class MyAppModelTest
{
    [Test]
    public void StartLoadingUserCausesMyAppModelToDoStuffWithOtherDep()
    {
        var model = new MyAppModel();
        var itemLoaded = new ManualResetEventSlim(initialState: false);
        model.User.ItemLoaded += (s, e) => itemLoaded.Set();

        model.StartLoadingUser();
        itemLoaded.Wait();

        // At this point we are guaranteed that MyAppModel.HandleUserLoaded 
        // has finished execution
        myServiceMock.Received().AmazingServiceCall();
    } 
}