【问题标题】:Unsubscribing from anonymous event handler inside an static method (extension method)取消订阅静态方法(扩展方法)内的匿名事件处理程序
【发布时间】:2013-02-11 16:48:28
【问题描述】:

我有一个扩展方法来订阅实现INotifyPropertyChanged 的对象的PropertyChanged 事件。

我希望事件只触发一次。不多了。

这是我的方法。

public static void OnPropertyChanged<T>(this  INotifyPropertyChanged target, string    propertyName, Action action)
{
    if (target == null)
    {
        return;
    }

    PropertyChangedEventHandler handler = (obj, e) =>
    {

        if (propertyName == e.PropertyName)
        {
            action();
        }

    };


    target.PropertyChanged -= handler;
    target.PropertyChanged += handler;

}

但它不起作用。我无法删除事件处理程序,因此每次调用此方法时都会触发事件。

我尝试了不同的方法。而不是使用匿名方法,而是使用更传统的方法,例如:

public static void OnPropertyChanged<T>(this  INotifyPropertyChanged target, string    propertyName, Action action)
{
    if (target == null)
    {
        return;
    }

    target.PropertyChanged -= target_PropertyChanged;
    target.PropertyChanged += target_PropertyChanged;

}

static void target_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //do stuff here
    }

而且效果很好。该事件只触发一次,但我还需要 Action 参数。我不能用这种方法使用它。

任何解决方法或不同的方法来解决这个问题?静态方法中的匿名方法有什么奇怪的吗?

提前致谢。

【问题讨论】:

    标签: c# event-handling extension-methods static-methods


    【解决方案1】:

    这是使用匿名方法作为事件处理程序的限制。它们不能像普通方法(技术上是通过方法组转换自动创建的委托实例)那样被删除,因为匿名方法被编译到编译器生成的容器类中,并且每次都会创建该类的新实例。

    为了保留 action 参数,您可以创建一个容器类,其中包含事件处理程序的委托。该类可以在您正在使用的其他类的内部声明为私有 - 或设为内部,可能在“Helpers”命名空间中。它看起来像这样:

    class DelegateContainer
    {
        public DelegateContainer(Action theAction, string propName)
        {
             TheAction = theAction;
             PopertyName = propName;
        }
    
        public Action TheAction { get; private set; }
        public string PropertyName { get; private set; }
    
        public void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
        {
            if(PropertyName == e.PropertyName)
                TheAction();
        }
    }
    

    然后,在您的类中创建并存储对容器的引用。您可以创建一个静态成员 currentContainer,然后像这样设置处理程序:

    private static DelegateContainer currentContainer;
    
    public static void OnPropertyChanged<T>(this  INotifyPropertyChanged target, string    propertyName, Action action)
    {
       if (target == null)
       {
           return;
       }
    
       if(currentContainer != null)         
           target.PropertyChanged -= currentContainer.PropertyChangedHandler;
    
       currentContainer = new DelegateContainer(action, propertyName);
       target.PropertyChanged += currentContainer.PropertyChangedHandler;
    }
    

    【讨论】:

    • +1 表示容器类的想法,但我认为这太复杂了:Dictionary&lt;string, PropertyChangedEventHandler&gt; 可以为每个属性名称保存一个委托。 (是的,这是一种不同的容器类。)
    • 是的,我只是想介绍一个通用的解决方案。我隐含地试图传达的是,带有闭包的匿名方法会在后台生成一个容器类,其中包含对捕获变量的引用。所以本质上它是相同类型的行为,但你会明确..
    【解决方案2】:

    如果您取消订阅事件处理程序本身,您可以让您的第一个示例工作。

    public static void OnPropertyChanged<T>(this  INotifyPropertyChanged target, string    propertyName, Action action)
    {
        if (target == null)
        {
            return;
        }
    
        // Declare the handler first, in order to create
        // a concrete reference that you can use from within
        // the delegate
        PropertyChangedEventHandler handler = null;  
        handler = (obj, e) =>
        {
            if (propertyName == e.PropertyName)
            {
                obj.PropertyChanged -= handler; //un-register yourself
                action();
            }
    
        };
        target.PropertyChanged += handler;
    }
    

    上面的代码用作“一次性完成”事件处理程序。您可以注册无限数量的这些,并且每个在取消注册之前只会执行一次。

    请记住,如果您在多个线程中短时间连续引发事件,则这些处理程序之一可能会执行多次。为了防止这种情况,您可能需要创建一个静态Dictionary(T,T) 将对象实例映射到“锁定对象”,并添加一些哨兵代码以确保处理程序只执行一次。但是,这些实现细节似乎有点超出您当前编写的问题的范围。

    【讨论】:

    • 这是不同的行为。这只是让它被触发一次,而 OP 的代码试图确保只有一个处理程序(但它可以被触发多次)。
    • 哦,请注意,如果在短时间内从多个线程触发此事件,它可能会运行多次。
    • 所有优点。可能需要对象->处理程序的静态字典,以将对象的每个实例限制为单个事件处理程序。
    • 如果 PropertyChanged 是从多个线程触发的,那么您会遇到更大的问题。
    • 重新阅读原始问题,听起来 OP 想要的是一个“一劳永逸”的事件处理程序,它会自行取消订阅。可能需要一个静态字典映射对象实例到“锁定对象”,以确保一个处理程序只在多个线程中执行一次,以及一些“哨兵”代码来检查处理程序是否已经执行。
    【解决方案3】:

    从技术上讲,这与您尝试取消订阅的匿名方法不同。每次调用OnPropertyChanged 时,.NET 都会创建该方法的新实例。这就是为什么取消订阅不起作用的原因。

    【讨论】:

    • 这不一定只是技术上的不同方法。如果出于某种原因碰巧使用相同propertyName 的不同string 实例,则它必须使用不同的方法。
    猜你喜欢
    • 1970-01-01
    • 2021-03-29
    • 2010-09-16
    • 2010-11-19
    • 2018-10-06
    • 2011-09-12
    • 1970-01-01
    • 1970-01-01
    • 2020-01-31
    相关资源
    最近更新 更多