【问题标题】:How to keep track of event subscriptions for various events如何跟踪各种事件的事件订阅
【发布时间】:2019-03-10 10:09:39
【问题描述】:

考虑下面的例子。此示例有一个简单的事件源 (Ticker) 和一个通过更新表单标题的委托订阅其事件 (Ticked) 的表单。为简单起见,此示例只有一个事件,但请考虑多个表单订阅的大量事件的情况。

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();
            }
        }
    }
}

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

【问题讨论】:

    标签: c# events delegates event-handling eventhandler


    【解决方案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;
                }
            }
        }
    }
    

    【讨论】:

    • 第一个解决方案不起作用,因为ClearSubscriptions() 将清除所有订阅,而不仅仅是form 添加的订阅。第二个解决方案是我目前正在做的事情。我为每个消费者的每个事件保留了一个列表,因此我一直在寻找更优雅的解决方案。
    • @Chris 有一个适合您的解决方案 - 我已经更新了我的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-22
    • 2015-01-17
    • 1970-01-01
    • 2011-12-14
    • 2013-11-03
    • 1970-01-01
    相关资源
    最近更新 更多