Skip to content
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

[Proposal] DelegateWeakEventManager #92

Closed
2 of 10 tasks
brminnick opened this issue Sep 26, 2021 · 2 comments
Closed
2 of 10 tasks

[Proposal] DelegateWeakEventManager #92

brminnick opened this issue Sep 26, 2021 · 2 comments
Assignees
Labels
champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature in discussion proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail

Comments

@brminnick
Copy link
Collaborator

DelegateWeakEventManager

  • Proposed
  • Prototype
  • Implementation: Not Started
    • iOS Support
    • Android Support
    • macOS Support
    • Windows Support
  • Unit Tests: Not Started
  • Sample: Not Started
  • Documentation: Not Started

Summary

An event Delegate implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.

Inspired by Xamarin.Forms.WeakEventManager, expanding the functionality of Xamarin.Forms.WeakEventManager to support Delegate events

Detailed Design

DelegateWeakEventManager.shared.cs

public class DelegateWeakEventManager
{
  readonly Dictionary<string, List<Subscription>> eventHandlers = new Dictionary<string, List<Subscription>>();

  public void AddEventHandler(Delegate? handler, [CallerMemberName] string eventName = "")
  {
	ArgumentNullException.ThrowIfNull(eventName);
	ArgumentNullException.ThrowIfNull(handler)

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	EventManagerService.AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
  }

  public void RemoveEventHandler(Delegate? handler, [CallerMemberName] string eventName = "")
  {
	if (IsNullOrWhiteSpace(eventName))
		throw new ArgumentNullException(nameof(eventName));

	if (handler == null)
		throw new ArgumentNullException(nameof(handler));

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	EventManagerService.RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
  }

  public void HandleEvent(object? sender, object eventArgs, string eventName) => RaiseEvent(sender, eventArgs, eventName);

  public void HandleEvent(string eventName) => RaiseEvent(eventName);

  public void RaiseEvent(object? sender, object eventArgs, string eventName) =>
	EventManagerService.HandleEvent(eventName, sender, eventArgs, eventHandlers);

  public void RaiseEvent(string eventName) => EventManagerService.HandleEvent(eventName, eventHandlers);
}

EventManagerService.shared.cs

static class EventManagerService
{
  internal static void AddEventHandler(in string eventName, in object? handlerTarget, in MethodInfo methodInfo, in Dictionary<string, List<Subscription>> eventHandlers)
  {
	var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var targets);
	if (!doesContainSubscriptions || targets == null)
	{
		targets = new List<Subscription>();
		eventHandlers.Add(eventName, targets);
	}

	if (handlerTarget == null)
		targets.Add(new Subscription(null, methodInfo));
	else
		targets.Add(new Subscription(new WeakReference(handlerTarget), methodInfo));
  }

  internal static void RemoveEventHandler(in string eventName, in object? handlerTarget, in MemberInfo methodInfo, in Dictionary<string, List<Subscription>> eventHandlers)
  {
	var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var subscriptions);
	if (!doesContainSubscriptions || subscriptions == null)
		return;

	for (var n = subscriptions.Count; n > 0; n--)
	{
		var current = subscriptions[n - 1];

		if (current.Subscriber?.Target != handlerTarget
			|| current.Handler.Name != methodInfo?.Name)
		{
			continue;
		}

		subscriptions.Remove(current);
		break;
	}
  }

  internal static void HandleEvent(in string eventName, in object? sender, in object? eventArgs, in Dictionary<string, List<Subscription>> eventHandlers)
  {
	AddRemoveEvents(eventName, eventHandlers, out var toRaise);

	for (var i = 0; i < toRaise.Count; i++)
	{
		try
		{
			var (instance, eventHandler) = toRaise[i];
			if (eventHandler.IsLightweightMethod())
			{
				var method = TryGetDynamicMethod(eventHandler);
				method?.Invoke(instance, new[] { sender, eventArgs });
			}
			else
			{
				eventHandler.Invoke(instance, new[] { sender, eventArgs });
			}
		}
		catch (TargetParameterCountException e)
		{
			throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event Action` use `HandleEvent(string eventName)` or if invoking an `event Action<T>` use `HandleEvent(object eventArgs, string eventName)`instead.", e);
		}
	}
  }

  internal static void HandleEvent(in string eventName, in object? actionEventArgs, in Dictionary<string, List<Subscription>> eventHandlers)
  {
	AddRemoveEvents(eventName, eventHandlers, out var toRaise);

	for (var i = 0; i < toRaise.Count; i++)
	{
		try
		{
			var (instance, eventHandler) = toRaise[i];
			if (eventHandler.IsLightweightMethod())
			{
				var method = TryGetDynamicMethod(eventHandler);
				method?.Invoke(instance, new[] { actionEventArgs });
			}
			else
			{
				eventHandler.Invoke(instance, new[] { actionEventArgs });
			}
		}
		catch (TargetParameterCountException e)
		{
			throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object? sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(string eventName)`instead.", e);
		}
	}
  }

  internal static void HandleEvent(in string eventName, in Dictionary<string, List<Subscription>> eventHandlers)
  {
	AddRemoveEvents(eventName, eventHandlers, out var toRaise);

	for (var i = 0; i < toRaise.Count; i++)
	{
		try
		{
			var (instance, eventHandler) = toRaise[i];
			if (eventHandler.IsLightweightMethod())
			{
				var method = TryGetDynamicMethod(eventHandler);
				method?.Invoke(instance, null);
			}
			else
			{
				eventHandler.Invoke(instance, null);
			}
		}
		catch (TargetParameterCountException e)
		{
			throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object? sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action<T>` use `HandleEvent(object eventArgs, string eventName)`instead.", e);
		}
	}
  }

  static void AddRemoveEvents(in string eventName, in Dictionary<string, List<Subscription>> eventHandlers, out List<(object? Instance, MethodInfo EventHandler)> toRaise)
  {
	var toRemove = new List<Subscription>();
	toRaise = new List<(object?, MethodInfo)>();

	var doesContainEventName = eventHandlers.TryGetValue(eventName, out var target);
	if (doesContainEventName && target != null)
	{
		for (var i = 0; i < target.Count; i++)
		{
			var subscription = target[i];
			var isStatic = subscription.Subscriber == null;

			if (isStatic)
			{
				toRaise.Add((null, subscription.Handler));
				continue;
			}

			var subscriber = subscription.Subscriber?.Target;

			if (subscriber == null)
				toRemove.Add(subscription);
			else
				toRaise.Add((subscriber, subscription.Handler));
		}

		for (var i = 0; i < toRemove.Count; i++)
		{
			var subscription = toRemove[i];
			target.Remove(subscription);
		}
	}
  }
  
  static DynamicMethod? TryGetDynamicMethod(in MethodInfo rtDynamicMethod)
  {
	var typeInfoRTDynamicMethod = typeof(DynamicMethod).GetTypeInfo().GetDeclaredNestedType("RTDynamicMethod");
	var typeRTDynamicMethod = typeInfoRTDynamicMethod?.AsType();

	if (typeInfoRTDynamicMethod != null && typeInfoRTDynamicMethod.IsAssignableFrom(rtDynamicMethod.GetType().GetTypeInfo()))
		return (DynamicMethod?)typeRTDynamicMethod?.GetRuntimeFields()?.FirstOrDefault(f => f?.Name is "m_owner")?.GetValue(rtDynamicMethod);
	else
		return null;
  }

  static bool IsLightweightMethod(this MethodBase method)
  {
	var typeInfoRTDynamicMethod = typeof(DynamicMethod).GetTypeInfo().GetDeclaredNestedType("RTDynamicMethod");
	return method is DynamicMethod || (typeInfoRTDynamicMethod?.IsAssignableFrom(method.GetType().GetTypeInfo()) ?? false);
  }
}

Usage Syntax

XAML Usage

N/A

C# Usage

readonly DelegateWeakEventManager _canExecuteChangedEventManager = new DelegateWeakEventManager();

public event EventHandler CanExecuteChanged
{
    add => _canExecuteChangedEventManager.AddEventHandler(value);
    remove => _canExecuteChangedEventManager.RemoveEventHandler(value);
}

void OnCanExecuteChanged() => _canExecuteChangedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));

Unresolved Questions

Could this be promoted to .NET MAUI?

@brminnick brminnick added proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature labels Sep 26, 2021
@brminnick brminnick self-assigned this Sep 26, 2021
@brminnick
Copy link
Collaborator Author

Currently pending approval for promotion into .NET MAUI:
dotnet/maui#2703

@brminnick brminnick added help wanted This proposal has been approved and is ready to be implemented and removed help wanted This proposal has been approved and is ready to be implemented labels Oct 4, 2021
@brminnick
Copy link
Collaborator Author

Merged into .NET MAUI dotnet/maui#2703 (comment)

@github-actions github-actions bot locked and limited conversation to collaborators Nov 24, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature in discussion proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail
Projects
None yet
Development

No branches or pull requests

1 participant