检查事件是否至少触发了一次

3

我正在追踪一个事件是否已被触发,代码如下:

bool IsFormLoaded;
private void Form1_Load(object sender, EventArgs e)
{
    //Do stuff
    IsFormLoaded = true;
}
private void button1_Click(object sender, EventArgs e)
{
    //Do stuff
}

但是对于许多事件来说,这样做并不优雅,因此我希望找到一种解决方案,让我可以检查是否触发了任何事件,如下所示:

bool IsFormLoaded = IsEventFired(Form1_Loaded);
bool IsButton1Clicked = IsEventFired(Button1_Click);

你对触发事件感兴趣还是处理事件? - Hamlet Hakobyan
@scartag,OP并不是在寻找一个保证发生的事件,而是想要找到一种方法来确定任意给定的事件是否已经触发。 - O. R. Mapper
@O.R.Mapper 哦..我的错..没有仔细阅读问题。 - scartag
2
不,没有一种通用的方法来做到这一点(除了处理所有事件并保持得分)。最好回溯一下为什么(你认为)需要这样做。 - H H
你可以简单地检查事件任务是否已经发生。 例如,点击一个按钮使标签变红,那么你的检查就是标签是否为红色。 - Ashley Medway
你可以创建一个对象列表 List<object> objects = new List<object>,然后在事件触发时将发送者添加到列表中。objects.add(sender) 然后你可以检查该发送者是否在列表中...假设每个对象都会触发一个事件...这也最终会占用大量内存! - Ashley Medway
4个回答

5
您正在使用设计器处理事件。例如,您可以在构造函数中按以下方式执行操作:
this.Load += delegate { IsFormLoaded = true; };
button1.Click += delegate { IsButton1Clicked = true; };

在我看来,这更加优雅 :)


当您订阅必须被处理的对象的事件时,它也可能导致内存泄漏。 - Sten Petrov
@StenPetrov 嗯,为什么应用程序退出前应该处理掉它? - VladL
3
这只是让表单引用自身,以避免任何泄漏。 - H H
@StenPetrov,如果是“this”,则不可能发生内存泄漏。 - VladL
但是在 IsFormLoaded = true; 或者 IsButton1Clicked = true; 之前我需要做一些事情,将它们添加到委托中并不好。 - Elmo
显示剩余4条评论

1
有趣的问题,对我来说似乎是你不想一遍又一遍地写同样的东西。这就是为什么我宁愿选择单个通用组件,而不是哈希集或类似的东西。此外,由于表单实现通常基于线程,我使用并发字典。
这个解决方案可以通过几种不同的方式进行改进;最明显的是使处理更加通用,缺少0参数处理程序。我尽可能简单地保持了清晰度。也许我会在几天后在我的博客上发布更完整的内容;如果我这样做了,我会在这里分享信息。
我的解决方案有两个部分:(1)一个通用的钩子类和(2)表单中的实现。目前,该解决方案是懒惰的,例如,我将事件处理程序放在队列的末尾,而不是在队列的前面。您应该能够通过使用GetInvocationList或类似的东西来解决这个问题。
通用钩子类基本上是钩住事件并跟踪事件是否被调用:
public class EventHooks
{
    private class EventHooksEquality : IEqualityComparer<Tuple<string, object>>
    {
        public bool Equals(Tuple<string, object> x, Tuple<string, object> y)
        {
            return x.Item1.Equals(y.Item1) && object.ReferenceEquals(x.Item2, y.Item2);
        }

        public int GetHashCode(Tuple<string, object> obj)
        {
            return obj.Item1.GetHashCode();
        }
    }

    private ConcurrentDictionary<Tuple<string, object>, bool> called =
        new ConcurrentDictionary<Tuple<string, object>, bool>(new EventHooksEquality());

    private abstract class BaseHookHandler
    {
        protected BaseHookHandler(object container, string eventName, EventHooks hooks)
        {
            this.hooks = hooks;
            this.container = container;
            this.eventName = eventName;
        }

        protected string eventName;
        protected object container;
        protected EventHooks hooks;
    }

    private class HookHandler<T1> : BaseHookHandler
    {
        public HookHandler(object container, string eventName, EventHooks hooks)
            : base(container, eventName, hooks)
        {
        }
        public void Handle(T1 t1)
        {
            hooks.called.TryAdd(new Tuple<string, object>(eventName, container), true);
        }
    }

    private class HookHandler<T1, T2> : BaseHookHandler
    {
        public HookHandler(object container, string eventName, EventHooks hooks)
            : base(container, eventName, hooks)
        {
        }
        public void Handle(T1 t1, T2 t2)
        {
            hooks.called.TryAdd(new Tuple<string, object>(eventName, container), true);
        }
    }
    // add more handlers here...

    public void HookAll(object obj)
    {
        foreach (var eventHandler in obj.GetType().GetEvents()) 
        {
            Hook(obj, eventHandler.Name);
        }
    }

    public void Hook(object obj, string eventHandler)
    {
        if (obj == null)
        {
            throw new Exception("You have to initialize the object before hooking events.");
        }

        // Create a handler with the right signature
        var field = obj.GetType().GetEvent(eventHandler);
        var delegateInvoke = field.EventHandlerType.GetMethod("Invoke");
        Type[] parameterTypes = delegateInvoke.GetParameters().Select((a) => (a.ParameterType)).ToArray();

        // Select the handler with the correct number of parameters
        var genericHandler = Type.GetType(GetType().FullName + "+HookHandler`" + parameterTypes.Length);
        var handlerType = genericHandler.MakeGenericType(parameterTypes);
        var handlerObject = Activator.CreateInstance(handlerType, obj, eventHandler, this);
        var handler = handlerType.GetMethod("Handle");

        // Create a delegate
        var del = Delegate.CreateDelegate(field.EventHandlerType, handlerObject, handler);

        // Add the handler to the event itself
        field.AddEventHandler(obj, del);
    }

    public bool IsCalled(object obj, string eventHandler)
    {
        return called.ContainsKey(new Tuple<string, object>(eventHandler, obj));
    }
}

在类中的使用方法如下(示例):

   public Form1()
    {
        InitializeComponent();

        hooks.HookAll(this);
        // or something like: hooks.Hook(this, "Load");
        hooks.Hook(button1, "Click");

    }

    private EventHooks hooks = new EventHooks();

    private void Form1_Load(object sender, EventArgs e)
    {
    }

    private void button1_Click(object sender, EventArgs e)
    {
        this.textBox1.Text = 
            string.Format("Load: {0}\r\nClick: {1}\r\nButton click: {2}\r\n",
            hooks.IsCalled(this, "Load"),
            hooks.IsCalled(this, "Click"),
            hooks.IsCalled(button1, "Click"));
    }

这不可能是认真的吧...如果事件在你的钩子放置之前就被触发了怎么办?这个类还应该控制它希望实例化的类型的实例,以便在创建对象之前提前查找事件,然后至少可以做一些有意义的诡计...除非包括获取你不知道的事件的代码,否则这只是浪费时间或者半个解决方案! - Jay
在构造函数中调用HookAll?无论您是使用“+=”还是反射进行事件绑定,您的评论都适用于所有类型的事件绑定。 - atlaste
  1. 你本可以创建一个与所需函数签名相同的委托,并在调用委托之前调用所需函数。你的模式没有增加任何东西。
  2. 你需要获取InvocationList,然后重新添加现有的函数。
最后,你也可以做其他不好的事情,比如修改CLR。
- Jay
我确实提到了调用列表,而且如果你没有注意到的话,我确实使用了相同签名的委托。就我个人而言,我有点困惑你的问题是什么,因为问题是是否有一种方法可以知道哪些事件被触发 - 而在我提供的对象范围内,我正好提供了这个。 - atlaste
但是你的示例仅在事件公开时才能工作,尽管您可以轻松更改绑定标志。我的问题只是回答中所宣传的“完整性”,实际上根据您的观点而定,可能有些离谱。我很高兴原始帖子发现它有用。问候。 - Jay

0

编写自己的基本表单(派生自Windows.Forms.Form),并覆盖事件触发方法以捕获是否已触发事件。通过拥有一个基类,您将能够在所有表单中重用您的事件监视逻辑。

这里是一些示例代码,您可以使用它们。我只在此处使用了Loaded事件。您将不得不为您想要监视的所有事件执行此操作。您还可以使用枚举而不是使用常量。希望这可以帮助到您。

        const string OnLoadFired = "OnLoadFired";
        const string OnShownFired = "OnShownFired";
        List<string> eventsFired = new List<string>();

        protected override void OnLoad(EventArgs e)
        {
            if(!eventsFired.Contains(OnLoadFired))
            {
                eventsFired.Add(OnLoadFired);
            }
            base.OnLoad(e);
        }

        public bool IsEventFired(string eventName)
        {
            return eventsFired.Contains(eventName);
        }

这仍然只是一个字符串列表,可以在任何事件中完成,没有必要使用const string OnLoadFired = "OnLoadFired"; - Ashley Medway
  1. 我不会在这里使用 List,你应该使用 HashSet
  2. 不要存储字符串,如果必须存储,请至少存储事件的确切名称。
  3. 如果您附加事件处理程序而不是覆盖事件触发方法,则可以在第一次触发后取消附加事件处理程序,这对于任何频繁触发的事件(即任何键盘/鼠标相关事件)都值得做。
- Servy
同意这仍然是字符串列表,但我想不到其他方法。如果按事件级别执行,如果您希望以其他形式执行,则无法使用它。 - Dhawalk
@Servy。同意第1点和第2点。第3点也是我的第一反应,但提问者只需要知道事件是否被触发。取消订阅事件可能会在没有原始开发人员的情况下导致维护问题。 - Dhawalk
1
通常来说,维护代码的团队与编写代码的团队不是同一个团队。如果将来决定在第二次触发此事件时执行更多操作,则可能会忽略取消订阅的事件。 - Dhawalk

0

和Dhawalk的答案类似。在我写这篇文章之前,我没有看到那个答案。

        private HashSet<string> events = new HashSet<string>();
        private void IsLoaded(object sender, RoutedEventArgs e)
        {
            // check
            System.Diagnostics.Debug.WriteLine(CheckEvents("IsLoaded", true).ToString());
            // add
            System.Diagnostics.Debug.WriteLine(CheckEvents("IsLoaded", false).ToString());
            // check
            System.Diagnostics.Debug.WriteLine(CheckEvents("IsLoaded", true).ToString());
        }

        private bool CheckEvents(string Event, bool CheckAdd)
        {
            // CheckAdd True to check
            // CheckAdd Fasle to add
            bool result = events.Contains(Event);
            if (!result && !CheckAdd) events.Add(Event);
            return result;
        }

运行得非常好!我对此进行了一点修改以适应我的validationEvent,谢谢!现在我可以使用hashset在调用事件的方法中设置条件! - Harvey Lin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接