-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: Add weak event listener helper class #61517
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
I remember there is weak event helper in WPF assemblies. The weak event listener should get better integration with GC. For example, it should not require explicit clean up, and should clean up automatically when GC triggers. |
I like it and would be a great feature for library creators |
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsBackground and motivationIt's quite common in MVVM scenarios, that you will bind your models and viewmodels to UI controls. These UI controls will often listen for PropertyChanged and CollectionChanged events to update the UI dynamically. However since the UI views are quite often transient but the model data is long lived, you often see very expensive UI controls getting stuck in memory due to the event handler not getting unsubscribed. This problem is of course not limited to UI components only, but where you'll often see large costs associated with not unsubscribing from the events. A typical pattern for this is to use a helper class to subscribe to weak event handlers, and over and over again we see different implementations of this in various libraries. So much so recently .NET MAUI decided to graduate their toolkit helper to the main MAUI library. This got me thinking that since this is such a common scenario, that .NET should provide this at a lower level for all libraries to use. Earlier I had suggested that this should be a language feature, but it was concluded this should be an API feature: dotnet/roslyn#101 Example of various implementations:
API ProposalI think there are quite a few different approaches that can be taken, as shown with all the implementations above. I'd refer to the .NET MAUI example to start with: public void AddEventHandler(Delegate? handler, [CallerMemberName] string eventName = null)
{
ArgumentNullException.ThrowIfNull(eventName);
ArgumentNullException.ThrowIfNull(handler)
var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");
AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
}
public void RemoveEventHandler(Delegate? handler, [CallerMemberName] string eventName = "")
{
ArgumentNullException.ThrowIfNull(eventName);
ArgumentNullException.ThrowIfNull(handler)
var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");
RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
} API Usagereadonly WeakEventManager weakEventManager = new WeakEventManager();
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
add => weakEventManager.AddEventHandler(value);
remove => weakEventManager.RemoveEventHandler(value);
}
void OnPropertyChanged([CallerMemberName] in string propertyName = "") => weakEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(INotifyPropertyChanged.PropertyChanged)); Alternative DesignsWindows Community Toolkit: var weakEvent = new WeakEventListener<INotifyCollectionChanged, object, NotifyCollectionChangedEventArgs>(valNotifyCollection)
{
OnEventAction = (instance, source, args) => obj.SetActive(IsNullOrEmpty(instance)),
OnDetachAction = (weakEventListener) => valNotifyCollection.CollectionChanged -= weakEventListener.OnEvent
}; RisksNo response
|
+1 Related to #18645 as well. |
Looked more into this a bit from our Windows Community Toolkit code vs. the MAUI one recently. I think both APIs are required (or a solution to the problems they're trying to solve). They're not 'alternate' or competing implementations, they both try to solve the problem from different sides of the
So, I think both patterns are required for different scenarios? Don't think there's a singular API that can support both scenarios easily? It could be nice if there was just some syntactic sugar for both sides to make either of these scenarios work... like on the implementors side to prevent being captured have it be declared as a weak event: public weak event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged; Or also on the subscribing side: object.PropertyChanged ~= HandlePropertyChanged; (Just picked All the extra overhead of not capturing references would then just be handled by the runtime without the extra overhead (and risk for mis-implementation of the pattern, which is error prone) on the developer. This is a hard pattern to understand, nuanced, hard to detect when done wrong, and easy to mis-code. It could be also better optimized in the case that a weak event subscriber tries to register to an already weak event this way. But at least there'd be a way for either side of an event to not capture a reference and have it be weak without the other side having to do anything or a bunch of complex patterns and knowledge required outside of 'wanting a weak reference'. |
Wanted to provide the current var inpc = rowGroupInfo.CollectionViewGroup as INotifyPropertyChanged;
var weakPropertyChangedListener = new WeakEventListener<DataGrid, object, PropertyChangedEventArgs>(this) {
OnEventAction = static (instance, source, eventArgs) => instance.CollectionViewGroup_PropertyChanged(source, eventArgs),
OnDetachAction = (weakEventListener) => inpc.PropertyChanged -= weakEventListener.OnEvent // Use Local References Only
}
inpc.PropertyChanged += weakPropertyChangedListener.OnEvent; |
I've been looking into some performance implications of MAUI's Take for example usage, like in public event EventHandler<AppThemeChangedEventArgs> RequestedThemeChanged
{
add => _weakEventManager.AddEventHandler(value);
remove => _weakEventManager.RemoveEventHandler(value);
} This event fires when Dark or Light mode changes on each platform, and you can data-bind the result via the However, what happens in practice is that:
A general "weak event" pattern in .NET would be nice here. But I would be more interested to know how it could be implemented in a performant way. If the design is a new C# compiler feature like Or is this only possible to implement when combined with a new runtime feature? |
@jonathanpeppers I'm wondering if a weak subscribing side approach has a better performance than a weak publisher (implementor) side as your example from MAUI. I have implemented the weak subscribing side approach here which works very similar to the WPF I believe it might be faster than the MAUI implementation because it does not use reflection or open delegates. Example usage for public class Publisher : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
}
public class Subscriber
{
public void Init(Publisher publisher)
{
// Instead of publisher.PropertyChanged += Handler; use the following statement:
WeakEvent.PropertyChanged.Add(publisher, Handler)
}
public void Handler(object? sender, PropertyChangedEventArgs e) { }
} More details can be found on this Wiki page Weak Event. |
One of the issues I see, it seems every implementation involves two I don't see how to make "weak events" in the ~same performance/ballpark as a vanilla C# event. |
Context: dotnet#12130 Context: https://github.com/angelru/CvSlowJittering Profiling a customer sample app, I noticed a lot of time spent in `{AppThemeBinding}` and `WeakEventManager` while scrolling: 2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr... 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() 2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string) 16% is a *lot* to notice while scrolling. Sometimes I've made improvements where I only shaved off 3% of the total time. What is going on here is: * Default `maui` template has lots of `{AppThemeBinding}` in the default `Styles.xaml`. This supports Light vs Dark theming. * `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged` * Making every MAUI view subscribe to this event -- potentially multiple times. * Subscribers are a `Dictionary<string, List<Subscriber>>`, where there is a dictionary lookup followed by a O(N) search for unsubscribe operations. I spent a little time investigating if we can make a faster `WeakEventManager`, in general: dotnet/runtime#61517 I did not immediately see a way to make "weak events" fast, but I did see a way to make this scenario fast. Before: * For any `{AppThemeBinding}`, it calls both: * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time * `RequestedThemeChanged += OnRequestedThemeChanged` constant time * Where the `-=` is notably slower, due to possibly 100s of subscribers. After: * Create an `_attached` boolean, so we know know the "state" if it is attached or not. * New bindings only call `+=`, where `-=` will now only be called by `{AppThemeBinding}` in *rare* cases. * Most .NET MAUI apps do not "unapply" bindings, but `-=` would only be used in that case. After this change, the following method disappeared from `dotnet-trace` output completely: 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0% (basically no time) is spent inside this method. I also could notice a difference in general startup time of the sample app. An average of 10 runs on a Pixel 5: Before: Average(ms): 967.7 Std Err(ms): 4.62132737064436 Std Dev(ms): 14.6139203045133 After: Average(ms): 958.9 Std Err(ms): 3.22645316098034 Std Dev(ms): 10.2029407525478 So I could notice a ~10ms improvement to startup in this app, and scrolling seemed a bit better as well. Note that I don't think this completely solves dotnet#12130, as things still seem sluggish to me when scrolling. But it is a reasonable improvement to start with that benefits all .NET MAUI apps on all platforms.
* [controls] fix performance issue in {AppThemeBinding} Context: #12130 Context: https://github.com/angelru/CvSlowJittering Profiling a customer sample app, I noticed a lot of time spent in `{AppThemeBinding}` and `WeakEventManager` while scrolling: 2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr... 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() 2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string) 16% is a *lot* to notice while scrolling. Sometimes I've made improvements where I only shaved off 3% of the total time. What is going on here is: * Default `maui` template has lots of `{AppThemeBinding}` in the default `Styles.xaml`. This supports Light vs Dark theming. * `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged` * Making every MAUI view subscribe to this event -- potentially multiple times. * Subscribers are a `Dictionary<string, List<Subscriber>>`, where there is a dictionary lookup followed by a O(N) search for unsubscribe operations. I spent a little time investigating if we can make a faster `WeakEventManager`, in general: dotnet/runtime#61517 I did not immediately see a way to make "weak events" fast, but I did see a way to make this scenario fast. Before: * For any `{AppThemeBinding}`, it calls both: * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time * `RequestedThemeChanged += OnRequestedThemeChanged` constant time * Where the `-=` is notably slower, due to possibly 100s of subscribers. After: * Create an `_attached` boolean, so we know know the "state" if it is attached or not. * New bindings only call `+=`, where `-=` will now only be called by `{AppThemeBinding}` in *rare* cases. * Most .NET MAUI apps do not "unapply" bindings, but `-=` would only be used in that case. After this change, the following method disappeared from `dotnet-trace` output completely: 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0% (basically no time) is spent inside this method. I also could notice a difference in general startup time of the sample app. An average of 10 runs on a Pixel 5: Before: Average(ms): 967.7 Std Err(ms): 4.62132737064436 Std Dev(ms): 14.6139203045133 After: Average(ms): 958.9 Std Err(ms): 3.22645316098034 Std Dev(ms): 10.2029407525478 So I could notice a ~10ms improvement to startup in this app, and scrolling seemed a bit better as well. Note that I don't think this completely solves #12130, as things still seem sluggish to me when scrolling. But it is a reasonable improvement to start with that benefits all .NET MAUI apps on all platforms. * PR feedback
Context: #12130 Context: https://github.com/angelru/CvSlowJittering Profiling a customer sample app, I noticed a lot of time spent in `{AppThemeBinding}` and `WeakEventManager` while scrolling: 2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr... 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() 2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string) 16% is a *lot* to notice while scrolling. Sometimes I've made improvements where I only shaved off 3% of the total time. What is going on here is: * Default `maui` template has lots of `{AppThemeBinding}` in the default `Styles.xaml`. This supports Light vs Dark theming. * `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged` * Making every MAUI view subscribe to this event -- potentially multiple times. * Subscribers are a `Dictionary<string, List<Subscriber>>`, where there is a dictionary lookup followed by a O(N) search for unsubscribe operations. I spent a little time investigating if we can make a faster `WeakEventManager`, in general: dotnet/runtime#61517 I did not immediately see a way to make "weak events" fast, but I did see a way to make this scenario fast. Before: * For any `{AppThemeBinding}`, it calls both: * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time * `RequestedThemeChanged += OnRequestedThemeChanged` constant time * Where the `-=` is notably slower, due to possibly 100s of subscribers. After: * Create an `_attached` boolean, so we know know the "state" if it is attached or not. * New bindings only call `+=`, where `-=` will now only be called by `{AppThemeBinding}` in *rare* cases. * Most .NET MAUI apps do not "unapply" bindings, but `-=` would only be used in that case. After this change, the following method disappeared from `dotnet-trace` output completely: 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0% (basically no time) is spent inside this method. I also could notice a difference in general startup time of the sample app. An average of 10 runs on a Pixel 5: Before: Average(ms): 967.7 Std Err(ms): 4.62132737064436 Std Dev(ms): 14.6139203045133 After: Average(ms): 958.9 Std Err(ms): 3.22645316098034 Std Dev(ms): 10.2029407525478 So I could notice a ~10ms improvement to startup in this app, and scrolling seemed a bit better as well. Note that I don't think this completely solves #12130, as things still seem sluggish to me when scrolling. But it is a reasonable improvement to start with that benefits all .NET MAUI apps on all platforms.
* [controls] fix performance issue in {AppThemeBinding} Context: #12130 Context: https://github.com/angelru/CvSlowJittering Profiling a customer sample app, I noticed a lot of time spent in `{AppThemeBinding}` and `WeakEventManager` while scrolling: 2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr... 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() 2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string) 16% is a *lot* to notice while scrolling. Sometimes I've made improvements where I only shaved off 3% of the total time. What is going on here is: * Default `maui` template has lots of `{AppThemeBinding}` in the default `Styles.xaml`. This supports Light vs Dark theming. * `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged` * Making every MAUI view subscribe to this event -- potentially multiple times. * Subscribers are a `Dictionary<string, List<Subscriber>>`, where there is a dictionary lookup followed by a O(N) search for unsubscribe operations. I spent a little time investigating if we can make a faster `WeakEventManager`, in general: dotnet/runtime#61517 I did not immediately see a way to make "weak events" fast, but I did see a way to make this scenario fast. Before: * For any `{AppThemeBinding}`, it calls both: * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time * `RequestedThemeChanged += OnRequestedThemeChanged` constant time * Where the `-=` is notably slower, due to possibly 100s of subscribers. After: * Create an `_attached` boolean, so we know know the "state" if it is attached or not. * New bindings only call `+=`, where `-=` will now only be called by `{AppThemeBinding}` in *rare* cases. * Most .NET MAUI apps do not "unapply" bindings, but `-=` would only be used in that case. After this change, the following method disappeared from `dotnet-trace` output completely: 2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents() Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0% (basically no time) is spent inside this method. I also could notice a difference in general startup time of the sample app. An average of 10 runs on a Pixel 5: Before: Average(ms): 967.7 Std Err(ms): 4.62132737064436 Std Dev(ms): 14.6139203045133 After: Average(ms): 958.9 Std Err(ms): 3.22645316098034 Std Dev(ms): 10.2029407525478 So I could notice a ~10ms improvement to startup in this app, and scrolling seemed a bit better as well. Note that I don't think this completely solves #12130, as things still seem sluggish to me when scrolling. But it is a reasonable improvement to start with that benefits all .NET MAUI apps on all platforms. * PR feedback --------- Co-authored-by: Jonathan Peppers <[email protected]>
Background and motivation
It's quite common in MVVM scenarios, that you will bind your models and viewmodels to UI controls. These UI controls will often listen for PropertyChanged and CollectionChanged events to update the UI dynamically. However since the UI views are quite often transient but the model data is long lived, you often see very expensive UI controls getting stuck in memory due to the event handler not getting unsubscribed. This problem is of course not limited to UI components only, but where you'll often see large costs associated with not unsubscribing from the events.
A typical pattern for this is to use a helper class to subscribe to weak event handlers, and over and over again we see different implementations of this in various libraries. So much so recently .NET MAUI decided to graduate their toolkit helper to the main MAUI library. This got me thinking that since this is such a common scenario, that .NET should provide this at a lower level for all libraries to use.
Earlier I had suggested that this should be a language feature, but it was concluded this should be an API feature: dotnet/roslyn#101
Example of various implementations:
WeakEventListener
on Github reveals C# is quite common to have this:WeakEvent
API Proposal
I think there are quite a few different approaches that can be taken, as shown with all the implementations above. I'd refer to the .NET MAUI example to start with:
API Usage
Alternative Designs
Windows Community Toolkit:
WeakEventListener.cs
Risks
No response
The text was updated successfully, but these errors were encountered: