如何跟踪各种事件的事件订阅

时间:2019-03-10 10:09:39

标签: c# events delegates event-handling eventhandler

考虑下面的示例。此示例具有一个简单的事件源(Ticker)和一个通过更新表单标题的委托来订阅其事件的表单(已勾选)。为了简单起见,此示例仅包含一个事件,但请考虑存在多个事件的多种形式的情况。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;

namespace ConsoleApp3
{
    class Program
    {
        // event cannot be used as a key type
        // Dictionary<event, List<EventHandler>> subscriptions = new Dictionary<event, List<EventHandler>>();

        static void Main(string[] args)
        {
            Ticker ticker = new Ticker();
            Form form = new Form();
            form.Show();
            EventHandler eventHandler = new EventHandler((s, e) => {
                form.Invoke(new Action(() => { form.Text = DateTime.Now.ToString(); }));
            });
            // save a reference to the event and the delegate to be added
            //if (!subscriptions.ContainsKey(ticker.Ticked))
            //    subscriptions.Add(ticker.Ticked, new List<EventHandler>());
            //subscriptions[ticker.Ticked].Add(eventHandler);
            //form.FormClosing += (s, e) => {
            //    foreach (KeyValuePair<event, List<EventHandler>> subscription in subscriptions)
            //        foreach (EventHandler eventHandler in subscription.Value)
            //            subscription.Key -= eventHandler;
            //};
            //finally subscribe to the event(s)
            ticker.Ticked += eventHandler;
            Application.Run();
        }

        class Ticker
        {
            public event EventHandler Ticked;

            public Ticker()
            {
                new Thread(new ThreadStart(() => {
                    while (true)
                    {
                        Ticked?.Invoke(null, null);
                        Thread.Sleep(1000);
                    }
                })).Start();
            }
        }
    }
}

如何将表单订阅的事件以及由表单添加到每个事件的委托保存在集合中,以便可以在关闭表单之前将其删除?

1 个答案:

答案 0 :(得分:1)

简短的回答:这是不可能的。您无法将事件存储在集合中,以后再清除其处理程序列表。

事件仅用于一种目的-封装。他们唯一要做的就是提供访问器(即addremove),因此类外部的代码只能将处理程序添加/删除到后备委托字段。这些代码基本上是相同的:

class MyClass
{
    public event EventHandler MyEvent;
}
class MyClass
{
    private EventHandler myDelegate;

    public event EventHandler MyEvent
    {
        add => myDelegate += value;
        remove => myDelegate -= value;
    }
}

但是假设我们不使用事件,而是直接使用委托。您可以创建一个字典,其中的键是委托而不是事件,但这对您的问题不起作用。这是因为代理是不可变的。您不能将委托存储在集合中,然后检索它并清除其调用列表。

这里唯一的解决方案是直接引用每个事件,就像这段代码一样。我不确定该解决方案是否适合您。

using System;
using System.Threading;
using System.Windows.Forms;

namespace ConsoleApp3
{
    class Program
    {
        static void Main()
        {
            var ticker = new Ticker();
            var form = new Form();
            form.Show();

            form.FormClosing += (s, e) => ticker.ClearSubscriptions();

            ticker.Ticked += new EventHandler((s, e) => form.Invoke(
                new Action(() => form.Text = DateTime.Now.ToString())));

            Application.Run();
        }

        class Ticker
        {
            public event EventHandler Ticked;

            public Ticker()
            {
                new Thread(new ThreadStart(() => {
                    while (true)
                    {
                        Ticked?.Invoke(this, EventArgs.Empty);
                        Thread.Sleep(1000);
                    }
                })).Start();
            }

            public void ClearSubscriptions()
            {
                Ticked = null;
            }
        }
    }
}

如您所见,ClearSubscriptions手动清除Ticked事件。如果您还有更多事件,则还必须手动清除它们,并且只能在Ticker类中进行清除,因为这是唯一可以访问基础委托的位置。您只能清除自己声明的事件。

或者,您可以为每个事件存储一个单独的列表。

static void Main()
{
    var ticker = new Ticker();
    var form = new Form();
    form.Show();

    var tickedSubscriptions = new List<EventHandler>();

    form.FormClosing += (s, e) =>
    {
        foreach (var subscription in tickedSubscriptions)
        {
            ticker.Ticked -= subscription;
        }

        tickedSubscriptions.Clear();
    };

    var handler = new EventHandler((s, e) => form.Invoke(
        new Action(() => form.Text = DateTime.Now.ToString())));

    tickedSubscriptions.Add(handler);
    ticker.Ticked += handler;

    Application.Run();
}

但是我认为这种解决方案并不理想,因为您必须跟踪许多单独的列表。

更新:

我想到了另一种适用于您的情况的解决方案。我不确定它是否优雅。

即使委托是不可变的,也没有什么可以阻止我们创建可以更改后备委托并将包装器放入字典的包装器对象。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;

namespace ConsoleApp3
{
    class Program
    {
        private static Dictionary<EventHandlerWrapper, List<EventHandler>> subscriptions =
        new Dictionary<EventHandlerWrapper, List<EventHandler>>();

        static void Main()
        {
            var ticker = new Ticker();
            var form = new Form();
            form.Show();

            form.FormClosing += (s, e) =>
            {
                foreach (var subscription in subscriptions)
                {
                    foreach (var handler in subscription.Value)
                    {
                        subscription.Key.Remove(handler);
                    }
                }

                subscriptions.Clear();
            };

            var updateTitle = new EventHandler((s, e) =>
                form.Invoke(new Action(() => form.Text = DateTime.Now.ToString())));

            ticker.Ticked += updateTitle;
            subscriptions.Add(ticker.TickedWrapper, new List<EventHandler> { updateTitle });

            Application.Run();
        }

        class Ticker
        {
            public event EventHandler Ticked;
            public EventHandlerWrapper TickedWrapper;

            public Ticker()
            {
                TickedWrapper = new EventHandlerWrapper(
                    () => Ticked,
                    handler => Ticked += handler,
                    handler => Ticked -= handler);

                new Thread(new ThreadStart(() => {
                    while (true)
                    {
                        Ticked?.Invoke(this, EventArgs.Empty);
                        Thread.Sleep(1000);
                    }
                })).Start();
            }
        }

        class EventHandlerWrapper
        {
            public Func<EventHandler> Get { get; }
            public Action<EventHandler> Add { get; }
            public Action<EventHandler> Remove { get; }

            public EventHandlerWrapper(
                Func<EventHandler> get,
                Action<EventHandler> add,
                Action<EventHandler> remove)
            {
                this.Get = get;
                this.Add = add;
                this.Remove = remove;
            }
        }
    }
}