Attach event handler to be called only once

这一生的挚爱 提交于 2019-12-13 03:40:06

问题


I am currently trying to write an extension function to be able to easily attach an action that is only used once when the event is fired, then unsubscribed.

I am trying something like this:

public static void AttachOnce<TEventArgs>([NotNull] this EventHandler<TEventArgs> me, [NotNull] Action<object, TEventArgs> action)
    where TEventArgs : System.EventArgs
{
    var handler = me;
    EventHandler<TEventArgs> wrappedAction = null;
    wrappedAction = (sender, args) =>
    {
        action(sender, args);
        handler -= wrappedAction;
    };
    handler += wrappedAction;
}

But ReSharper complains on the unsubscribe that handler is "Access to modified closure".

I know what this means, so I made the local variable for the closure already, but it doesn't seem to resolve it. What is failing here?

The direct hard-coded code works. Something like this:

var file = new FileSystemWatcher("path/to/file");

FileSystemEventHandler handler = null;
handler = (sender, args) =>
{
    // My action code here
    file.Changed -= handler;
};
file.Changed += handler;

EDIT 1 (2018-10-09 11:43 CET):

I may just have been too fast, asking a question before thoroughly thinking it through.
You can't create extension methods on Events. At all. It's just not possible in C#. So I can't even test why ReSharper is complaining and if it is right, because a call like file.Changed.AttachOnce(action) is not valid. It says "The event 'Changed' can only appear on the left hand side of += or -=".

I have found some more sources for similar requests/questions:
http://www.hardkoded.com/blog/csharp-wishlist-extension-for-events
One time generic event call?


回答1:


I've been thinking about a different but much simpler approach, using a "self-detaching" inline handler which would be used like this:

obj.Event += (s, e) =>
{
    Detach(obj, nameof(obj.Event));
    // ..do stuff..
};

The Detach method would look like this and could be put anywhere you like (most likely a static helper class):

public static void Detach(object obj, string eventName)
{
    var caller = new StackTrace().GetFrame(1).GetMethod();
    var type = obj.GetType();
    foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
    {
        if (typeof(Delegate).IsAssignableFrom(field.FieldType))
        {
            var handler = (field.GetValue(obj) as Delegate)?.GetInvocationList().FirstOrDefault(m => m.Method.Equals(caller));
            if (handler != null)
            {
                type.GetEvent(eventName).RemoveEventHandler(obj, handler);
                return;
            }
        }
    }
}

So for your example the code would look like this:

file.Changed += (s, e) =>
{
    Detach(file, nameof(file.Changed));
    // My action code here
};



回答2:


In this case, it's okay. ReSharper simply warns that handler is different between the time you declare and the time it is executed.




回答3:


Not sure how exactly you want to design your extension method, but maybe this will get you started:

public static void OnChangedOnce(this FileSystemWatcher instance, Action<object, FileSystemEventArgs> action)
{
    var file = instance;
    var wrappedAction = action;
    FileSystemEventHandler handler = null;
    handler = (object sender, FileSystemEventArgs args) =>
    {
        wrappedAction(sender, args);
        file.Changed -= handler;
    };
    file.Changed += handler;
    file.EnableRaisingEvents = true; // mandatory
}

var file = new FileSystemWatcher("path/to/file");
file.OnChangedOnce((sender, args)) =>
{
    // your action here
});


来源:https://stackoverflow.com/questions/52716939/attach-event-handler-to-be-called-only-once

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!