Edit
I've added a second pass at this problem based on the answers that have been posted by other users so far: https://codereview.stackexchange.com/a/221525/75659
Context
Before presenting the code for review, I want to be clear that I am not looking for recommendations on how to avoid having to solve the issue that the code is trying to solve in the first place
This is not production code, this is purely for exploratory purposes, and I would prefer answers that remain within the provided requirements.
With that said, let's suppose I have this contrived interface:
public interface IFoo<T>
{
event Action<T> ChangeValue;
event Action<object> ChangeObject;
event Action ChangeEmpty;
void InvokeChange(T value);
}
With the following contrived requirements:
Calling
InvokeChange
with a value of typeT
thatFoo
was constructed with will invoke the three eventsChangeValue
is invoked with the given value as typeT
ChangeObject
is invoked with the given value as typeobject
ChangeEmpty
is invoked with no arguments
Handlers that listen to any of the three events are invoked in the global order they were added, regardless of what event they were added to.
- Effectively there is only a single event here - some 'change' happened - but 3 event signatures that can be utilized by listeners as needed.
The implementation can take any form, but must implement
IFoo<T>
as it is specified above.The implementation does not need to be thread safe and can allocate memory, but no memory should leak when it is used correctly.
Example 1
The order that event listeners are called should correspond to the order in which they were added, regardless of which event they were added to:
IFoo<int> test = new Foo<int>();
test.ChangeEmpty += () => Console.WriteLine("0) Empty!");
test.ChangeObject += (o) => Console.WriteLine("1) Object: {0}", o);
test.ChangeValue += (v) => Console.WriteLine("2) Value: {0}", v);
test.ChangeObject += (o) => Console.WriteLine("3) Object: {0}", o);
test.ChangeEmpty += () => Console.WriteLine("4) Empty!");
test.InvokeChange(123);
Should print the following to the Console:
0) Empty!
1) Object: 123
2) Value: 123
3) Object: 123
4) Empty!
Example 2
Adding/Removing event Listeners should work as expected, while still maintaining the same order:
public static void Main()
{
IFoo<int> test = new Foo<int>();
test.ChangeEmpty += EmptyHandler;
test.ChangeObject += ObjectHandler;
Console.WriteLine("1) EMPTY, OBJECT");
test.InvokeChange(1);
test.ChangeEmpty -= EmptyHandler;
test.ChangeValue += ValueHandler;
Console.WriteLine("2) OBJECT, VALUE");
test.InvokeChange(2);
test.ChangeObject -= ObjectHandler;
Console.WriteLine("3) VALUE ");
test.InvokeChange(3);
test.ChangeObject += ObjectHandler;
test.ChangeEmpty += EmptyHandler;
test.ChangeValue += ValueHandler;
Console.WriteLine("4) VALUE, OBJECT, EMPTY, VALUE");
test.InvokeChange(4);
test.ChangeValue -= ValueHandler;
test.ChangeValue -= ValueHandler;
test.ChangeEmpty -= EmptyHandler;
test.ChangeObject -= ObjectHandler;
Console.WriteLine("5) <NONE>");
test.InvokeChange(5);
}
static void EmptyHandler() { Console.WriteLine(" - Empty!"); }
static void ObjectHandler(object val) { Console.WriteLine(" - Object: {0}", val); }
static void ValueHandler(int val) { Console.WriteLine(" - Value: {0}", val); }
Should print the following to the Console:
1) EMPTY, OBJECT
- Empty!
- Object: 1
2) OBJECT, VALUE
- Object: 2
- Value: 2
3) VALUE
- Value: 3
4) VALUE, OBJECT, EMPTY, VALUE
- Value: 4
- Object: 4
- Empty!
- Value: 4
5) <NONE>
Effectively it should perform as if there is a single invokation list that backs all 3 events.
This code is not required to be particularly performant or thread safe, but 'correct'. In other words, invocation order and frequency is correct regardless of how many times a given handler is added/removed from one of the three events.
Code For Review
With all that in mind, the solution I came up with is:
ChangeValue
is a standard event backed by anAction<T>
ChangeObject
andChangeEmpty
are custom event accessors that provide a customadd
andremove
implementation which are ultimately also backed byChangeValue
.- The
value
ofChangeObject.add
andChangeEmpty.add
is wrapped in a lambda 'proxy' of signatureAction<T>
- To ensure that the event accessors are able to also remove the corresponding proxy when in
ChangeObject.remove
andChangeEmpty.remove
, a dictionary is used for each to track a stack of proxies that have been bound for each value added.add
pushes a proxy to the stack and is then added withChangeValue += proxy
remove
pops a proxy from the stack and is then removed withChangeValue -= proxy
- The
Here is the relevant code for one of the events:
public event Action<T> ChangeValue = delegate {};
private Dictionary<Action<object>, List<Action<T>>> ObjectEventProxies
= new Dictionary<Action<object>, List<Action<T>>>();
public event Action<object> ChangeObject
{
add
{
List<Action<T>> eventProxies;
if(!ObjectEventProxies.TryGetValue(value, out eventProxies))
{
eventProxies = new List<Action<T>>();
ObjectEventProxies.Add(value, eventProxies);
}
Action<T> proxy = (T v) => value.Invoke(v);
eventProxies.Add(proxy);
ChangeValue += proxy;
}
remove
{
List<Action<T>> eventProxies;
if (ObjectEventProxies.TryGetValue(value, out eventProxies))
{
Action<T> proxy = eventProxies[eventProxies.Count - 1];
eventProxies.RemoveAt(eventProxies.Count - 1);
ChangeValue -= proxy;
}
}
}
And the full code, including the above examples: https://dotnetfiddle.net/u7NGMJ
Question
This code fulfills the given requirements but my question is, can this be simplified? There's a fair bit of boilerplate here for something that feels like it should be simpler to do.
To re-iterate, I am looking specifically for answers that still meet all of the listed requirements, regardless of how contrived or unnecessary they might seem. I should be able to swap out my IFoo<T>
implementation for yours and have the output be identical.
I am aware I could just use a single Action<object>
if I wanted a single signature to work for all cases. That is not what this question is asking.
Edit
I've added a second pass at this problem based on the answers that have been posted by other users so far: https://codereview.stackexchange.com/a/221525/75659