Giter Site home page Giter Site logo

weakevent's Introduction

Hi, I'm Thomas Levesque

  • .NET developer from France, currently living in Québec City, Canada
  • Working mostly with C#, ASP.NET Core, Azure and Google Cloud Platform
  • Currently working at UEAT
  • Former Microsoft MVP from 2012 to 2023 ("Developer Technologies" category)
  • Maintainer of FakeItEasy, the easy mocking library for .NET

Follow me

Open-source portfolio

I tend to create many projects. Some of them are just experiments; others turn out to be useful to someone, and I do my best to maintain them. Here's a selection of the projects I felt were worth mentioning:

  • FakeItEasy: one of the most popular mocking libraries for .NET. Its simple concepts and fluent syntax make it very easy to use. I joined the core team in 2016.
  • DontMergeMeYet: a GitHub application that prevents you from accidentally merging a pull request before it's ready.
  • Extensions.Hosting.AsyncInitialization: a library to help with async app initialization in .NET Core generic host (.NET Core 2.1+ and ASP.NET Core 3).
  • Hamlet: a simple Option type for .NET with Linq support.
  • WeakEvent: a generic weak event implementation.
  • Linq.Extras: a collection of extension methods to complement the ones from System.Linq.Enumerable.
  • NString: a collection of utilities for working with strings in .NET.
  • NHotkey: a managed library to handle global hotkeys in Windows Forms and WPF applications.
  • EssentialMVVM: a minimalist MVVM Framework.
  • HumanBytes: a library to convert byte sizes to a human readable form.
  • Iso8601DurationHelper: A small library to handle ISO8601 durations in C#.
  • CosmosDB Studio: a tool to browse and query Azure CosmosDB databases (in progress)
  • WpfAnimatedGif: a library to display animated GIFs in WPF
  • XamlAnimatedGif: a library to display animated GIFs in XAML (UWP, WPF, Windows Phone, and even Silverlight!)
  • I also contribute frequently to various projects in .NET land: .NET Core, ASP.NET Core, Identity Server...

weakevent's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

weakevent's Issues

Reduce cost of events with no subscribers

Split from #30, in which @mhn65536 suggested that the cost of unused events (i.e. with no subscribers) could be reduced.

Although the approach suggested by @mhn65536 cannot be used due to thread-safety reasons, there's a way to make things better by lazily allocating the handlers collection. The WeakEventSource instance will still be allocated.

Raise problem with Multiple subscriptions on the same Event! Raise does not check if an handler still alive

Probably the title is not very clear. I try to explain better.

Yesterday I have seen a deadlock on using the WeakEventSource, I have seen that this problem should be fixed with the last version, and I confirm that the deadlock is solved, BUT, I have a new problem.
This is an easy example that explain my problem (a window with three buttons, one to subscribe custom events, one to unsubscribe, and the last to raise the event):

    private readonly WeakEventSource<EventArgs> _evTestSource = new WeakEventSource<EventArgs>();
    public event EventHandler<EventArgs> EventTest
    {
        add { _evTestSource.Subscribe(value); }
        remove { _evTestSource.Unsubscribe(value); }
    }
    
    private void RaiseEventTest()
    {
        _evTestSource?.Raise(this, EventArgs.Empty);
    }
    
    private void Subscribe_Click(object sender, RoutedEventArgs e)
    {
        EventTest += MainWindow_EventTest1;
        EventTest += MainWindow_EventTest2;
    }
    
    private void UnSubscribe_Click(object sender, RoutedEventArgs e)
    {
        EventTest -= MainWindow_EventTest1;
        EventTest -= MainWindow_EventTest2;
    }
    
    private void Raise_Click(object sender, RoutedEventArgs e)
    {
        RaiseEventTest();
    }
    
    private void MainWindow_EventTest1(object sender, EventArgs e)
    {
        Debug.WriteLine("INFO: MainWindow_EventTest1");
        EventTest -= MainWindow_EventTest2;
    }
    
    private void MainWindow_EventTest2(object sender, EventArgs e)
    {
        Debug.WriteLine("INFO: MainWindow_EventTest2");
    }

The WeakEvent EventTest is 'attached' with two handlers (MainWindow_EventTest1 and MainWindow_EventTest2).
If I raise the event, by the call of the RaiseEventTest method, the first handler 'theoretically' remove the second handler

     EventTest -= MainWindow_EventTest2;

But the Raise method of the WeakEventSource raise also the second handler that, theoretically, was removed from the previous handler.
Very hard to explain.
Probably it's necessary to verify, also in the loop used to call the invoke methods, if an handler still active.

Different behavior of standard event and WeakEventSource

Hello,
I have found following article about weak event patterns: https://www.codeproject.com/Articles/29922/Weak-Events-in-C#heading0013

There is an interesting point, try to find the text: "A possible problem here is that someone might try to attach an anonymous method as an event handler that captures a variable. ..."

I supposed that it must be general problem of similar implementation. So I tried to test your implementation using following code:

public class MyClass
{
    private readonly WeakEventSource<EventArgs> _myEventSource = new WeakEventSource<EventArgs>();

    public event EventHandler<EventArgs> WeakExecuted
    {
        add => _myEventSource.Subscribe(value);
        remove => _myEventSource.Unsubscribe(value);
    }

    public event EventHandler StrongExecuted;

    public void Execute()
    {
        _myEventSource.Raise(this, EventArgs.Empty);
        StrongExecuted?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main(string[] args)
    {
        MyClass x = new MyClass();
        RegisterWeakHandler(x);
        RegisterStrongHandler(x);
        System.GC.Collect();
        x.Execute();
    }

    private static void RegisterWeakHandler(MyClass x)
    {
        x.WeakExecuted += (s, a) => { OnWeakExecuted(x, a); };
    }

    private static void RegisterStrongHandler(MyClass x)
    {
        x.StrongExecuted += (s, a) => { OnStrongExecuted(x, a); };
    }

    private static void OnWeakExecuted(object sender, EventArgs e)
    {
        Console.WriteLine("OnWeakExecuted");
    }

    private static void OnStrongExecuted(object sender, EventArgs e)
    {
        Console.WriteLine("OnStrongExecuted");
    }
}

It seems that only OnStrongExecuted is written to the console. It implies that normal event behaves in a little bit different way than your WeakEventSource. Do you know about this difference? It could be crucial for some usages. Is there any solution for the problem? The mentioned article shows how to detect the situation.

operator overloading

You could use operator overloading to mimic the use(+= /-=) of normal events.
Also this would allow you to create an instance only on demand, reducing the cost of unused events, as the + operator can return a new instance if used on a null.

How to troubleshoot

I tried it in my application, but the listener is never called. How can I troubleshoot it?

Handling exception thrown by handlers

I am evaluating the library for an internal use at my company.

One issue I have currently is that if any handler throws an exception, it will prevent other handlers from receiving the same event. While that is currently the same behavior of normal .NET events, it can cause issues.

Would you consider an overloaded method for WeakEventSource<TEventArgs>.Raise that takes a delegate that can handle any exception and decide whether event notification should continue or not.

Something like:

public void Raise(object? sender, TEventArgs args, Func<Exception, bool> exceptionHandler)
{
    var validHandlers = GetValidHandlers(_handlers);
    foreach (var handler in validHandlers)
    {
        try
        {
            handler.Invoke(sender, args);
        }
        catch (Exception ex)
        {
            if (exceptionHandler(ex))
                continue;
            throw;
        }
    }
}

I was also thinking of a more advanced handler that returns a state so that bad subscriber can be automatically unsubscribed:

[Flags]
public enum ErrorHandlingFlags
{
    None = 0,
    NotThrow = 1,
    Unsubscribe = 2,
}

public void Raise(object? sender, TEventArgs args, Func<Exception, ErrorHandlingFlags> exceptionHandler)
{
    var validHandlers = GetValidHandlers(_handlers);
    foreach (var handler in validHandlers)
    {
        try
        {
            handler.Invoke(sender, args);
        }
        catch (Exception ex)
        {
            var flag = exceptionHandler(ex);
            if (flag.HasFlag(ErrorHandlingFlags.Unsubscribe))
            {
                // find a way to unsubscribe
            }

            if (flag.HasFlag(ErrorHandlingFlags.NotThrow))
                continue;

            throw;
        }
    }
}

But I'm not sure how the unsubscription can work without the original EventHandler<TEventArgs>.

anonymous delegates

As far as i can see, the code fails when using anonymous delegates, that capture variables, because those create a new class. I'm not very good at explaining, but you sure got the idea.

int i = 0;
Source.Subscribe((o,e) => Test(i));

You could provide an overload to Subscribe that allows you to specify the hook object instead of deriving it from delegate.Target. Obviously it'd most likely require the use of ConditionalWeakTable.

[Question] Are lambda event handlers available for the GC?

Hi,

This is more of a question so feel free to close it afterwards.

If we register a lambda event handler I assume it will be GCed because there's no one holding a strong reference to it, right?

I'm not too familiar with how lambdas work behind the scenes, so that's why I'm asking.

Thank you,
Cosmin

Event does not propogate when subscribed IoC

I created a parent class with two children. Child 1 fires a WeakEvent. Child 2 has a public handler. In the parent scope, we should be able to subscribe via inversion of control:

public Parent()
{
    Child1 = new Child1();
    Child2 = new Child2();
    Child1.Event += Child2.EventHandler;
}

I have very weird behavior with the event firing off and on. I have confirmed that a local handler inside Child1 is firing correctly. But Child2 doesn't fire as expected. It literally worked in one debug and failed in the next with no changes. Any ideas?

NuGet Package with signed assembly

I am using it right now in an strong-named assembly. As there is no signed version of the assembly in the package, I cannot start my application.

For now I'll do it by myself, but it would be nice if this was added to the package in the future.

Implementation for standard handlers.

Hello.

I needed to implement an event of type PropertyChangedEventHandler. But the above code only implements EventHandler<MyEventArgs>. I've modified your code a little to suit my needs, but maybe this can be implemented in the main project?

Below is an example using my code:

        private readonly WeakEventSource<PropertyChangedEventHandler, PropertyChangedEventArgs> _PropertyChangedEventSource = new();
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _PropertyChangedEventSource.Subscribe(value); }
            remove { _PropertyChangedEventSource.Unsubscribe(value); }
        }

Dead handlers are never removed from the DelegateCollection if the event is not raised

Hello!

Please check DelegateCollectionBase class for memory leak issue in Dictionary
private readonly Dictionary<int, List<int>> _index;

I think this behavior because this method:

 public void CollectDeleted()
        {         
         ....

Newer called.
In my code Unsubscribe and Raise method (that calls CollectDeleted method) is never called.
For fix this issue I think need also add call CollectDeleted method in Subscribe method on class WeakEventSourceHelper too.

Version 2.0 does not pass Windows App Cert Kit (UWP)

Hi,

I have been using your excellent WeakEvent project using NuGet for a while now. Yesterday I noticed that there is a version 2.0 released. I upgraded my NuGet packaged and released a new version of my UWP app to the Windows Store. This FAILED because of the following report.

Supported API test

FAILED
Supported APIs

**•Error Found: The supported APIs test detected the following errors:◦API FormatMessage in api-ms-win-core-localization-l1-2-0.dll is not supported for this application type. System.Diagnostics.Tracing.dll calls this API.**


•Impact if not fixed:  Using an API that is not part of the Windows SDK for Windows Store apps violates the Windows Store certification requirements. 


•How to fix:  Review the error messages to identify the API that is not part of the Windows SDK for Windows Store apps. Please note, apps that are built in a debug configuration or without .NET Native enabled (where applicable) can fail this test as these environments may pull in unsupported APIs. Retest your app in a release configuration, and with .NET Native enabled if applicable. See the link below for more information: 
 Alternatives to Windows APIs in Windows Store apps. 

After downgrading back to the old version it PASSED again. Somewhere you do some calls to System.Diagnostics.Tracing.dll which cause this problem. I hope this report helps.

Thanks

Fons Sonnemans

How to use WeakEventSource with GetInvocationList

Hi Thomas,

I use the WeakEventSource like this and it's work pretty well.

    {
        private WeakEventSource<PropertyChangedEventArgs> _weakPropertyChanged;
        private WeakEventSource<PropertyChangedEventArgs> WeakPropertyChanged => _weakPropertyChanged ??= new WeakEventSource<PropertyChangedEventArgs>();

        /// <summary>
        /// Property Changed event
        /// </summary>
        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                WeakPropertyChanged.Subscribe(value.ConvertDelegate<EventHandler<PropertyChangedEventArgs>>());
            }
            remove
            {
                WeakPropertyChanged.Unsubscribe(value.ConvertDelegate<EventHandler<PropertyChangedEventArgs>>());
            }
        }

But in my code with "normal" PropertyChange I use this what is now not possible:

            foreach (var methodDelegate in PropertyChanged.GetInvocationList())
            {
                try
                {
                    PropertyChangedEventHandler handler = methodDelegate as PropertyChangedEventHandler;
                    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
                    if (handler != null)
                    {
                        handler(this, e);
                    }
                }
                catch (Exception ex)
                {
                     // log the exception
                }
            }

Is this also possible with the WeakEventSource??

Greetings Michael

Poor performance of unsubscription

At ClosedXML/ClosedXML#535 we tried to use this library to avoid huge memory consumption caused by non-released delegates but faced with serious performance degradation.
As it turned out, the reason is in non-optimal way of unsubscription. I prepare an example reproducing the issue. Here I run the same test with different numbers of items. Inside the test method, I subscribe a certain number of times to the event, then unsubscribe half of the entries, and then raise the event.

As can be seen, time taken by subscription grows linearly, as well as time of raising the event (with more complicated delegates this was evident), while unsubscription time grows quadratically.

| Count |Subscribe| Raise | Unsubscribe |
|      4|    22 ms|   1 ms|         1 ms|
|      8|     0 ms|   0 ms|         0 ms|
|     16|     0 ms|   0 ms|         0 ms|
|     32|     0 ms|   0 ms|         0 ms|
|     64|     0 ms|   0 ms|         0 ms|
|    128|     0 ms|   0 ms|         0 ms|
|    256|     0 ms|   0 ms|         1 ms|
|    512|     0 ms|   0 ms|         3 ms|
|   1024|     1 ms|   0 ms|        13 ms|
|   2048|     3 ms|   0 ms|        52 ms|
|   4096|     9 ms|   0 ms|       263 ms|
|   8192|    19 ms|   0 ms|       948 ms|
|  16384|    35 ms|   0 ms|      3653 ms|
|  32768|    60 ms|   0 ms|     14163 ms|
|  65536|   136 ms|   3 ms|     65120 ms|
| 131072|   375 ms|   4 ms|    231021 ms|
| 262144|   706 ms|  14 ms|    863929 ms|

Indeed, a single removal from a list has a complexity of O(n), thus unsubscription of n/2 gives us complexity O(n²), which is quite bad.

I don't know which way to fix it is preferable. If subscribers were stored in a HashSet instead of a List this would benefit the unsubscription, but their order would become unpredictable, which (I suspect) is undesirable. All other ways which I can think of would cost either subscription or raising events becoming slower (O(log(n)) instead of O(1) and O(n*log(n)) instead of O(n)),

If you agree I can submit a PR after a while.

Code to reproduce:

        static void Main(string[] args)
        {
            var count = 4;
            Console.WriteLine("| Count |Subscribe| Raise | Unsubscribe |");
            for (int i = 0; i < 19; i++)
            {
                Test(count);
                count *= 2;
            }
            Console.ReadKey();
        }

        static void Test(int count)
        {
            var sw = new Stopwatch();
            sw.Start();

            var pub = new Publisher();

            var calledSubscribers = new List<int>();
            EventHandler<EventArgs> handler = null;
            var unsubscribeHandlers = new List<EventHandler<EventArgs>>();

            for (int i = 0; i < count; i++)
            {
                var i1 = i;
                EventHandler<EventArgs> h = (sender, e) => calledSubscribers.Add(i1);
                handler += h;
                if (i % 2 == 0)
                    unsubscribeHandlers.Add(h);
            }

            pub.Foo += handler;

            var subscribe = sw.ElapsedMilliseconds;
            sw.Restart();

            foreach (var unsubscribeHandler in unsubscribeHandlers)
            {
                pub.Foo -= unsubscribeHandler;
            }

            var unsubscribe = sw.ElapsedMilliseconds;
            sw.Restart();

            pub.Raise();
            var raise = sw.ElapsedMilliseconds;

            Console.WriteLine("|{0}|{1} ms|{2} ms|{3} ms|",
                count.ToString().PadLeft(7),
                subscribe.ToString().PadLeft(6),
                raise.ToString().PadLeft(4),
                unsubscribe.ToString().PadLeft(10)
                );
        }


        class Publisher
        {
            private readonly WeakEventSource<EventArgs> _fooEventSource = new WeakEventSource<EventArgs>();
            public event EventHandler<EventArgs> Foo
            {
                add => _fooEventSource.Subscribe(value);
                remove => _fooEventSource.Unsubscribe(value);
            }

            public void Raise()
            {
                _fooEventSource.Raise(this, EventArgs.Empty);
            }
        }

        class InstanceSubscriber
        {
            private readonly int _id;
            private readonly Action<int> _onFoo;

            public InstanceSubscriber(int id, Publisher pub, Action<int> onFoo)
            {
                _id = id;
                _onFoo = onFoo;
                pub.Foo += OnFoo;
            }

            private void OnFoo(object sender, EventArgs e)
            {
                _onFoo(_id);
            }

            public void Unsubscribe(Publisher pub)
            {
                pub.Foo -= OnFoo;
            }
        }

Change Unsubscribe to match the behavior of Delegate.Remove

Currently, when the Unsubscribe method receives a multicast delegate, it removes each individual handler from the weak event source. Since it has a role similar to Delegate.Remove, it should be consistent with the behavior of that method. Delegate.Remove removes the last occurrence of the whole invocation list of the specified delegate; if it doesn't find the exact list, it doesn't remove anything.

Suggested by @Weldryn in #11, see the discussion there for details.

How to clear all subscriptions?

Thanks for creating this library, I'm finding it very useful.

With a standard C# event handler you could clear all subscribers by setting it equal to null from within its class. Using WeakEventSource, is there a way to do something equivalent?

Maybe just creating a new WeakEventSource<> object is the way to do that?

Ideas for better performance and safety

Hello :)

I imagine you did some research before starting this project, and I guess you probably wouldn't have started the project if there were already a satisfying implementation available. I know it was a long time ago, but do you remember if you had the chance to review this: https://www.codeproject.com/Articles/29922/Weak-Events-in-C ?

The author seems trustworthy ("lead developer on SharpDevelop"), and the performance of his implementation is pretty much excellent. To achieve such performance he replaces reflection with System.Reflection.Emit.DynamicMethod and IL code.

  • Is there any particular reason you didn't use this technique in your library, when the running platform supports it? (beside it being black magic)

  • His implementation ensures you can't use anonymous delegates by checking for the presence of CompilerGeneratedAttribute on declaring type of the delegate's method. It could be a welcomed improvement in your library, because it's way too easy to erroneously subscribe to a lambda...


I put up the code-project's code in a new repo, and replaced the original ad-hoc benchmark with BenchmarkDotNet. I also added a benchmark for your library. Check it out! https://github.com/mlaily/FastSmartWeakEvent

Summary:

|                                 Method |      Mean |     Error |   StdDev | Ratio | RatioSD |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------------------- |----------:|----------:|---------:|------:|--------:|-------:|------:|------:|----------:|
|                'Normal (strong) event' |  12.12 ns |  0.218 ns | 0.414 ns |  1.00 |    0.00 |      - |     - |     - |         - |
|                     'Smart weak event' | 594.21 ns |  9.563 ns | 7.467 ns | 47.41 |    0.98 | 0.0420 |     - |     - |     177 B |
| 'Fast smart weak event (2009 version)' |  41.49 ns |  0.839 ns | 1.030 ns |  3.36 |    0.12 | 0.0172 |     - |     - |      72 B |
| 'Fast smart weak event (2013 version)' |  18.16 ns |  0.157 ns | 0.131 ns |  1.45 |    0.04 |      - |     - |     - |         - |
|                    'Thomas weak event' | 567.82 ns | 10.375 ns | 9.705 ns | 45.64 |    1.33 | 0.1545 |     - |     - |     650 B |

EDIT: I almost forgot: he apparently rewrote the thing 4 years after his code-project article, but it seems to only live in a gist here: https://gist.github.com/dgrunwald/6445360#file-fastsmartweakevent-cs (I added it to my repo and benchmark)

Have a great day!

Performance degradation

Hey,

This is one of those difficult ones... not sure if there's a solution.

I switched from the standard .NET WeakEventManager to your WeakEvent because I need something that is .NET Standard compliant. WeakEventManager exists only in the WindowsBase.dll assembly and isn't available in .NET Standard.

So your library works great, but the performance is severely degraded. I experience runtimes of 12x for a unit test where events are subscribed and unsubscribed.

I'll try to create a reduced test case to illustrate the issue. But in the meantime, I ran a CPU Usage diagnosis and I can see that it's partly the locking at https://github.com/thomaslevesque/WeakEvent/blob/master/WeakEvent/WeakEventSource.cs#L42 and mostly due to the expensive IsMatch at https://github.com/thomaslevesque/WeakEvent/blob/master/WeakEvent/WeakEventSource.cs#L121 .

I've started digging into MS's reference code to see why their pattern is fast, but their code is copyrighted and also not simple to understand.

Maybe you can see an obvious fix.

Using Task with Raise

My event handlers run async code.

When I raise my event handlers, I'm in an async context, so I can easily await.

What are your thoughts on a WeakEventSourceAsync with a Task Raise(object sender, TEventArgs e)?

edit: Typo with signature.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.