65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 WinRT 的 WeakEventManager

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (2投票s)

2014 年 6 月 16 日

CPOL

2分钟阅读

viewsIcon

9615

适用于 WinRT 的 WeakEventManager

引言

当使用实例方法注册到事件时,事件源会存储对事件处理程序的引用。这通常没问题,但是存在事件源可能比事件处理程序存活时间更长的情况。我最近遇到的一个例子是,我想监听 Clipboard.ContextChanged 事件,该事件是 static 的。

在 WPF 中,你会使用 WeakEventManager<TEventSource, TEventArgs> 类,但是这个类在 Windows Store 应用中不可用,所以让我们自己构建它吧!我稍微修改了 API,因为我需要重载来接受一个 Type,这是由于 Clipboard 是一个 static 类造成的(你不能将其用于泛型类型参数)。所以,这就是我最终用来注册事件的方式

WeakEventManager.AddHandler<object>(
    typeof(Clipboard),
    "ContentChanged",
    this.OnClipboardContentChanged);

这是主类——实际逻辑由嵌套类执行

using System;
using System.Collections.Generic;
using System.Reflection;

/// <summary>
/// Implements a weak event listener that allows the owner to be garbage
/// collected if its only remaining link is an event handler.
/// </summary>
public static class WeakEventManager
{
    private readonly static List<WeakEvent> registeredEvents = new List<WeakEvent>();

    /// <summary>
    /// Adds the specified event handler to the specified event.
    /// </summary>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="sourceType">
    /// The type of the class that contains the specified event.
    /// </param>
    /// <param name="eventName">The name of the event to subscribe to.</param>
    /// <param name="handler">The delegate that handles the event.</param>
    public static void AddHandler<TEventArgs>
        (Type sourceType, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = sourceType.GetRuntimeEvent(eventName);
        registeredEvents.Add(
            new WeakEvent(null, eventInfo, handler));
    }

    /// <summary>
    /// Adds the specified event handler to the specified event.
    /// </summary>
    /// <typeparam name="TEventSource">The type that raises the event.</typeparam>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="source">
    /// The source object that raises the specified event.
    /// </param>
    /// <param name="eventName">The name of the event to subscribe to.</param>
    /// <param name="handler">The delegate that handles the event.</param>
    public static void AddHandler<TEventSource, TEventArgs>
        (TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = typeof(TEventSource).GetRuntimeEvent(eventName);
        registeredEvents.Add(
            new WeakEvent(source, eventInfo, handler));
    }

    /// <summary>
    /// Removes the specified event handler from the specified event.
    /// </summary>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="sourceType">
    /// The type of the class that contains the specified event.
    /// </param>
    /// <param name="eventName">
    /// The name of the event to remove the handler from.
    /// </param>
    /// <param name="handler">The delegate to remove.</param>
    public static void RemoveHandler<TEventArgs>
        (Type sourceType, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = sourceType.GetRuntimeEvent(eventName);
        foreach (WeakEvent weakEvent in registeredEvents)
        {
            if (weakEvent.IsEqualTo(null, eventInfo, handler))
            {
                weakEvent.Detach();
                break;
            }
        }
    }

    /// <summary>
    /// Removes the specified event handler from the specified event.
    /// </summary>
    /// <typeparam name="TEventSource">The type that raises the event.</typeparam>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="source">
    /// The source object that raises the specified event, or null if it's
    /// a static event.
    /// </param>
    /// <param name="eventName">
    /// The name of the event to remove the handler from.
    /// </param>
    /// <param name="handler">The delegate to remove.</param>
    public static void RemoveHandler<TEventSource, TEventArgs>
        (TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = typeof(TEventSource).GetRuntimeEvent(eventName);
        foreach (WeakEvent weakEvent in registeredEvents)
        {
            if (weakEvent.IsEqualTo(source, eventInfo, handler))
            {
                weakEvent.Detach();
                break;
            }
        }
    }

    private class WeakEvent
    {
        // Implementation details to follow
    }
}

当然,在生产环境中,你希望进行一些参数验证(例如,不为 null,并且 GetRuntimeEvent 返回了某些内容),但是为了保持代码简单,我省略了它。实际逻辑位于 WeakEvent 嵌套类中。这是必要的,因为你不能仅仅保存对事件处理程序对象的弱引用,因为这很可能是一个临时对象,很快就会被垃圾回收。你也不能保存对事件处理程序的强引用,因为它引用了目标实例,会使其保持存活,从而违背了该类的目的!因此,WeakEvent 类必须以某种方式包装事件处理程序委托的各个部分,以便目标类可以被垃圾回收。以下是我采用的方法

private class WeakEvent
{
    private static readonly MethodInfo onEventInfo =
        typeof(WeakEvent).GetTypeInfo().GetDeclaredMethod("OnEvent");

    private EventInfo eventInfo;
    private object eventRegistration;
    private MethodInfo handlerMethod;
    private WeakReference<object> handlerTarget;
    private object source;

    public WeakEvent(object source, EventInfo eventInfo, Delegate handler)
    {
        this.source = source;
        this.eventInfo = eventInfo;

        // We can't store a reference to the handler (as that will keep
        // the target alive) but we also can't store a WeakReference to
        // handler, as that could be GC'd before its target.
        this.handlerMethod = handler.GetMethodInfo();
        this.handlerTarget = new WeakReference<object>(handler.Target);

        object onEventHandler = this.CreateHandler();
        this.eventRegistration = eventInfo.AddMethod.Invoke(
            source,
            new object[] { onEventHandler });

        // If the AddMethod returned null then it was void - to
        // unregister we simply need to pass in the same delegate.
        if (this.eventRegistration == null)
        {
            this.eventRegistration = onEventHandler;
        }
    }

    public void Detach()
    {
        if (this.eventInfo != null)
        {
            WeakEventManager.registeredEvents.Remove(this);

            this.eventInfo.RemoveMethod.Invoke(
                this.source,
                new object[] { eventRegistration });

            this.eventInfo = null;
            this.eventRegistration = null;
        }
    }

    public bool IsEqualTo(object source, EventInfo eventInfo, Delegate handler)
    {
        if ((source == this.source) && (eventInfo == this.eventInfo))
        {
            object target;
            if (this.handlerTarget.TryGetTarget(out target))
            {
                return (handler.Target == target) &&
                        (handler.GetMethodInfo() == this.handlerMethod);
            }
        }

        return false;
    }

    public void OnEvent<T>(object sender, T args)
    {
        object instance;
        if (this.handlerTarget.TryGetTarget(out instance))
        {
            this.handlerMethod.Invoke(instance, new object[] { sender, args });
        }
        else
        {
            this.Detach();
        }
    }

    private object CreateHandler()
    {
        Type eventType = this.eventInfo.EventHandlerType;
        ParameterInfo[] parameters = eventType.GetTypeInfo()
                                                .GetDeclaredMethod("Invoke")
                                                .GetParameters();

        return onEventInfo.MakeGenericMethod(parameters[1].ParameterType)
                            .CreateDelegate(eventType, this);
    }
}

基本上,委托的各个部分存储在两个部分中;要调用的方法(存储为普通引用)和要在其上调用该方法的 target(存储为弱引用)。棘手的部分是通过反射在事件上注册我们的处理程序,这由于核心 WinRT 类中的事件与 .NET 事件略有不同而变得更加困难(例如,事件的 add 方法返回一个 EventRegistrationToken,必须将其传递给 remove 方法;.NET 事件为 add 方法返回 void ,并且需要将传递到 add 方法的委托作为 remove 方法的参数)。当收到回调时,我们会快速检查我们的 target 是否仍然有效;如果无效,则从事件中注销(毕竟,在未来调用时我们没什么可做的!),这允许 GC 清理 WeakEvent 实例(尽管它不应该太重)。

归类于:CodeProject

© . All rights reserved.