Listen for event and invoke callback, based on specification?

Viewed 247

I am currently building out a custom task manager and I'm wondering if it's possible to tell the task manager to listen for a specific event (OnSomething below), and then invoke a callback method when the task raises that event. However, mentally I can't see how it's possible to listen for an event that doesn't exist at the base class level. For example, I have a base class that contains basic information about the task called CustomTask:

public abstract class CustomTask {
    public bool IsRunning { get; private set; } = false;
    public void Start() {
        IsRunning = true;
        DoSomething();
        IsRunning = false;
    }
    protected abstract void DoSomething();
}

For the sake of SO readers, I've simplified the definition, but you get the gist of it. It contains basic details, a few methods for starting and canceling, provides basic state management (simplified IsRunning here), etc.

I then have custom tasks that derive from CustomTask, in this case, let's focus on a sample task called CustomTaskA. It contains a definition for an event called OnSomething, which someone, somewhere may want to listen for:

public sealed class CustomTaskA : CustomTask {
    protected override void DoSomething() => RaiseOnSomething(this, new EventArgs());
    public event EventHandler<EventArgs> OnSomething;
    private void RaiseOnSomething(object sender, EventArgs e) => OnSomething?.Invoke(sender, e);
}

Now, the CustomTaskManager registers tasks, tracks them via Guid, manages them and more, but for simplicity:

public sealed class CustomTaskManager {
    // Singleton setup.
    private static CustomTaskManager _instance = new CustomTaskManager();
    public static CustomTaskManager Instance {
        get {
            // Simplified for SO.
            if (_instance == null)
                _instance = new CustomTaskManager();

            return;
        }
    }

    // Collection of tasks.
    private Dictionary<Guid, CustomTask> _tasks = new Dictionary<Guid, CustomTask>();

    // Register and start a task.
    public bool TryRegisterAndStartTask(CustomTask task, out Guid taskId) {
        taskId = Guid.Empty;
        try {
            // Register task.
            taskId = Guid.NewGuid();
            _tasks.Add(taskId, task);

            // Listen for events.

            // Start task.
            task.Start();
        } catch (Exception e) {
            // Log exception.
        }

        return false;
    }
}

When registering and starting a task, I'd like to tell the task manager I want to listen for OnSomething, and if OnSomething is invoked, I want the task manager to call a method OnSomethingWasRaised. For example:

TaskManager.Instance.TryRegisterAndStartTask(task, out Guid taskId, task.OnSomething, OnSomethingWasRaised);

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

I know the specifying and invoking a callback method is entirely possible, and listening for events is plausible with reflection.


Is there a way (with or without using reflection) to listen for a specified event defined on a derived object and then invoke a specified callback method?

NOTE: Please excuse any syntactical errors as I hand-typed the snippets to keep them minimal.

1 Answers

Problem with (proposed) approach like this:

TryRegisterAndStartTask(task, out Guid taskId, task.OnSomething, OnSomethingWasRaised);

is that you cannot pass event as argument, or store it in variable, because event is just a set of two methods (add and remove), just like property is a set of two methods get and set.

You can of course change event to "raw" delegate:

public EventHandler<EventArgs> OnSomething;

This one you can pass by reference:

public bool TryRegisterAndStartTask(CustomTask task, ref EventHandler<EventArgs> del, EventHandler<EventArgs> sub, out Guid taskId) {
        taskId = Guid.Empty;
        // subscribe
        del += sub;
        ...
} 

CustomTaskManager.Instance.TryRegisterAndStartTask(task, ref task.OnSomething, OnSomethingWasRaised, out var taskId);

But that's usually not a good idea, since you are losing private scope of events - with events one can only add\remove delegates, with raw delegate anyone can do anything, like invoking or setting to null.

If regular event stays - that means reflection is the only way to achieve your goal, and even worse - you'll have to reference to the event you want to subscribe to by string name, not by an actual reference, though you can use nameof(task.OnSomething). Then, you are losing compile time validation of subscription delegate type. Say you want to subscribe to event Action Something but passing Func<string> delegate there. It will compile fine with reflection approach, and fail only at runtime.

Still if you insist that will look something like this:

public bool TryRegisterAndStartTask(CustomTask task, string eventName, Delegate sub, out Guid taskId) {
    taskId = Guid.Empty;
    // subscribe
    var ev = task.GetType().GetEvent(eventName, BindingFlags.Public | BindingFlags.Instance);
    var addMethod = ev.GetAddMethod(); // this can be null or private by the way
    addMethod.Invoke(task, new [] {sub});
    ...
}

And called like this:

var task = new CustomTaskA();
EventHandler<EventArgs> handler = OnSomethingWasRaised;
CustomTaskManager.Instance.TryRegisterAndStartTask(task, nameof(task.OnSomething), handler, out var taskId);

Ugly, unsafe, and not worth it in your scenario, in my opinion.

Related