Our monolithic WinForm application is getting a face lift. One current challenge that we are restructuring how we setup events. I rolled my own event manager class to handle the subscriptions and unsubscriptions to prevent leaks whenever developers write code.
I ran into several challenges with storing generic values in non-generic types. So I came up with a pattern that allowed me to do it. Not sure if I am creating a code smell while doing it so I wanted someone to review and ask questions.
Base View
public abstract class View : Form, IView
{
protected readonly EventManager EventManager;
public View()
{
EventManager = new EventManager(this);
}
protected abstract void RegisterEvents();
}
Concrete Class
public class MyView : View
{
protected override void RegisterEvents()
{
EventManager.Attach(button1_Click, handler => button1.Click += handler, button1.Click -= handler);
}
}
Event Viewer
public class EventManager : IDisposable
{
private IList<TrackedEvent> Events = new List<TrackedEvent>();
private IDisposable Owner;
public EventManager(IDisposable owner)
{
Owner = owner;
}
public void Attach<TEventArgs>(EventHandler<TEventArgs> handler, Action<EventHandler<TEventArgs>> addEvent, Action<EventHandler<TEventArgs>> removeEvent = null)
{
Events.Add(GenericTrackedEvent<TEventArgs>.Register(@delegate, handler.Method.Name, removeEvent));
}
public void Attach(EventHandler handler, Action<EventHandler> addEvent, Action<EventHandler> removeEvent = null)
{
Events.Add(TrackedEvent.Register(@delegate, handler.Method.Name, removeEvent));
}
public void Dispose() { ... }
private class TrackedEvent : IDisposable
{
protected EventHandler Handler { get; set; }
protected string MethodName { get; set; }
protected Action<EventHandler> Unsubscription { get; set; }
protected TrackedEvent() { }
private TrackedEvent(EventHandler handler, string methodName, Action<EventHandler> removeEvent)
{
Handler = handler;
MethodName = methodName;
Unsubscription = removeEvent;
}
internal virtual void Remove()
{
Unsubscription(Handler);
}
internal static TrackedEvent Register(EventHandler handler, string methodName, Action<EventHandler> removeEvent)
{
return new TrackedEvent(handler, methodName, removeEvent);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
Handler = null;
Unsubscription = null;
}
}
private class GenericTrackedEvent<T> : TrackedEvent
{
protected new EventHandler<T> Handler { get; private set; }
protected new Action<EventHandler<T>> Unsubscription { get; private set; }
internal override void Remove()
{
Unsubscription(Handler);
}
private GenericTrackedEvent(EventHandler<T> handler, string methodName, Action<EventHandler<T>> removeEvent)
{
Handler = handler;
MethodName = methodName;
Unsubscription = removeEvent;
}
internal static TrackedEvent Register(EventHandler<T> handler, string methodName, Action<EventHandler<T>> removeEvent)
{
return new GenericTrackedEvent<T>(handler, methodName, removeEvent);
}
protected override void Dispose(bool disposing)
{
Handler = null;
Unsubscription = null;
base.Dispose(disposing);
}
}
}
So several things to point out and as to why:
- Some methods have been shortened for brevity.
- TrackedEvent holds onto the unsubscription e.g: the removeEvent handler.
- Previously implementations I struggled with storing generic types in a non-generic class without constant boxing/unboxing.
- The two static methods "Register" will return a TrackedEvent class because its constructors are private. I don't want anyone creating one outside of my implementation.
- TrackedEvent only handles non-generics.
- GenericTrackedEvent handles generics by hiding our base properties and overriding key properties.
- Unexpectedly, inheritance is working correctly when calling the remove method. My initial expectation was the GenericTrackedEvent was going to use the underlying TrackedEvent.Unsubscription and TrackedEvent.Handler properties but I was wrong - and that was okay.
- Someone is going to ask - why are you passing over the MethodName? Because I actually wrap my handlers in a new delegate so I can capture when event fire and log them to any source I choose. When I wrap my delegates in the attach method it carries that name over instead of the original one passed in the handler parameter. Instead of masking that method, I figured I'd keep it. Again some code was remove because I do not deem it relevant to the initial question.
Seems like we've implemented a duck typing but its usage is abstracted away from developers so I think its okay but wanted several other opinions to weight in.
Thoughts?
View
andMyView
classes are somehow 'lazy' clases (according to what you are showing us) however if in the future they will contain and provide more operations then it isn't a bad design. \$\endgroup\$TrackedEvent.Register
,GenericTrackedEvent<TEventArgs>.Register
nor@delegate
exists. \$\endgroup\$@delegate
does exist in the full implementation. I wrap the original handler with logging so we can track the usage of the event for debugging purposes Doing this causes the new delegate'sMethodName
to attach itself to that class, instead of the original handler.void @delegate(object s, TEventArgs e) { Debug.WriteLine($"\t{Owner.GetType()} event {handler.Method.Name}<{typeof(TEventArgs)}> has fired with {e}..."); handler(s, e); }
\$\endgroup\$