.net-coreblazormulticastdelegate

How can I somehow combine EventCallback<T>?


So I found myself in a situation where I need to subclass a few Blazor components, and one of the reasons is that I need to essentially create a decorator that extends it's functionality. Part of that extension is to tack on some additional event handling. Since components use EventCallback now which isn't a delegate type, I can't simply add another handler to it like you could with multicast delegates. I can replace it, but then that means any consumer of that component cannot register any handler of their own because mine would overwrite it, so now I'm trying to wrap it. Here's a pseudo representation of the scenario and what I'm trying to do

public class OriginalBlazorComponent : ComponentBase
{
    [Parameter]
    public EventCallback<int> SomethingChanged { get; set; }

    private async Task SomeInternalProcess()
    {
        // ... some work here
        await SomethingChanged.InvokeAsync(1);
    }
}

public class MySubclassedComponent : OriginalBlazorComponent
{
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        // I want to combine anything that the user may have registered with my own handling
        SomethingChanged = EventCallback.Factory.Create(this, async (int i) => 
        {
            // this causes a stack overflow because i just replaced it with this callback
            // so it's essentially calling itself by this point.
            await SomethingChanged.InvokeAsync(i); 

            await DoMyOwnStuff(i);
        });
        
        await base.SetParametersAsync(this);
    }
}

The idea here is that I'm just making sure that the user's handler has been bound by addressing this in SetParametersAsync() so that I can wrap it in a new callback that will call their handler first and then run mine after. But since it's the base component that has the property that gets invoked by the base class, that means I need to replace that specific property with my new handler, but in doing so, that means the new handler is calling the old handler which is actually now the new handler, so it's now an infinitely recursive call stack, and causes a stack overflow.

So my first thought was that if I could somehow get a copy of the original EventCallback, or at least extract its delegate so I can create a new callback, then it wouldn't be referencing itself anymore (confused because it's a struct, I thought it would always naturally be a copy), but I can't find any way to do that. I tried just using EventCallback.Factory.Create(this, SomethingChanged) in hopes that it would create a completely new instance of the callback using the same delegate, but it didn't change anything; same result.

This would of course be a non-issue if I could override the original component's SomeInternalProcess() method so that I could insert my process there before or after calling the base method, but it's a 3rd party library. Or if the SomethingChanged property itself was virtual I could override it to intercept its setter, but that is also not the case.

So in short, is there some way to achieve the same effect as a multicast delegate so that I can preserve any registered handlers but combine with my own? Or is there at least some way dereference the original EventCallback or extract its delegate so that I can create a new one?

e.g.

// how do I acheive something akin to
SomethingChanged += MyDelegate;

Update 1:

I tried "hiding" the SomethingChanged event callback by declaring my own on the child class so that I could register my own handler on the base which would include the user's handler in addition to my own. That worked in standard C# tests, but Blazor did not like it. It saw it as a duplicate property during render time and threw an exception.

Update 2:

Hackaroonie. EventCallback and EventCallback<T> both store the delegate in an internal field called Delegate. Just to see if it would work, I pulled it out via reflection and used it to create a new EventCallback that would replace the one created by the user, which would wrap both of ours together, executing theirs first and then mine. It works, and I haven't seen any strange side effects yet. But I hate it for obvious reasons. But it makes me wonder if maybe all I needed was for Microsoft to expose that field. I'm sure there's some sort of risk with it, but it's just a function pointer. So long as it's read-only, it should be fine right?


Solution

  • Ok so a simple chain of responsibility pattern actually worked:

    public class OriginalBlazorComponent : ComponentBase
    {
        [Parameter]
        public EventCallback<int> SomethingChanged { get; set; }
    
        private async Task SomeInternal Process()
        {
            // ... some work here
            await SomethingChanged.InvokeAsync(1);
        }
    }
    
    public class MySubclassedComponent : OriginalBlazorComponent
    {
        private EventCallback<int> _callerSomethingChanged;
    
        public override async Task SetParametersAsync(ParameterView parameters)
        {        
            parameters.SetParameterProperties(this);
    
            _callerSomethingChanged = SomethingChanged;
    
            SomethingChanged = EventCallback.Factory.Create(this, async (int i) => 
            {
                await _callerSomethingChanged.InvokeAsync(i);
                await DoMyStuff(i); // this is the decoration
            });
            
            await base.SetParametersAsync(ParameterView.Empty);
        }
    }
    

    This simply stores any callback that the user/developer may have defined in a private field and then replaces it with my callback which will invoke theirs and then mine. You have to call set parameters first so that their callback binds or else it'll end up overwriting yours. Then at the end, call base with an empty parameter view to indicate parameters are already set (it's a pattern within the framework, failing to do this will cause an exception).

    Now from the razor side, from the user/developer perspective, instead of using the original component like this:

    <OriginalBlazorComponent SomethingChanged="MyChangeHandler" />
    

    They can use the decorated component with the extended function

    <MySubclassedComponent SomethingChanged="MyChangeHandler" />
    

    And this does so without replacing their handler, it just makes sure that my handler gets called too. And also, from the base component's perspective (the third party original component), it just operates per normal like the subclass doesn't even exist.

    At the end of the day, EventCallback is what we use in Blazor, but apparently it can't be combined with other callbacks like we could with delegates (to my knowledge, anyway) otherwise this would have been pretty straight forward; I'd just add my handler to the delegate and be done with it. If someone knows a way to do that, I'd really want to know.

    And then for convenience, refactor as extension:

    public static class EventCallbackExt
    {
        public static EventCallback<T> Chain<T>(this EventCallback<T> original, IHandleEvent receiver, Action<T> del)
        {
            var chain = new EventCallbackChainCore<T>(receiver, original, del);
            return chain.Callback;
        }
    
        public static EventCallback<T> Chain<T>(this EventCallback<T> original, IHandleEvent receiver, Func<T, Task> del)
        {
            var chain = new EventCallbackChainCore<T>(receiver, original, del);
            return chain.Callback;
        }
    
        public static EventCallback Chain(this EventCallback original, IHandleEvent receiver, Action del)
        {
            var chain = new EventCallbackChainCore(receiver, original, del);
            return chain.Callback;
        }
    
        public static EventCallback Chain(this EventCallback original, IHandleEvent receiver, Func<Task> del)
        {
            var chain = new EventCallbackChainCore(receiver, original, del);
            return chain.Callback;
        }
    
        private class EventCallbackChainCore<T>
        {
            private readonly EventCallback<T> _original;
    
            public EventCallback<T> Callback { get; }
    
            public EventCallbackChainCore(IHandleEvent receiver, EventCallback<T> original, MulticastDelegate del)
            {
                _original = original;
    
                var chained = new EventCallbackWorkItem(del);
                Callback = EventCallback.Factory.Create(receiver, async (T input) =>
                {
                    await _original.InvokeAsync(input);
                    await chained.InvokeAsync(input);
                });
            }
        }
    
        private class EventCallbackChainCore
        {
            private readonly EventCallback _original;
    
            public EventCallback Callback { get; }
    
            public EventCallbackChainCore(IHandleEvent receiver, EventCallback original, MulticastDelegate del)
            {
                _original = original;
    
                var chained = new EventCallbackWorkItem(del);
                Callback = EventCallback.Factory.Create(receiver, async () =>
                {
                    await _original.InvokeAsync();
                    await chained.InvokeAsync(null);
                });
            }
        }
    }
    

    So this simplifies it for the future and would be used like this:

    public class MySubclassedComponent : OriginalBlazorComponent
    {
        public override async Task SetParametersAsync(ParameterView parameters)
        {        
            parameters.SetParameterProperties(this);
            SomethingChanged = SomethingChanged.Chain(this, DoMyStuff);
            return base.SetParametersAsync(ParameterView.Empty);
        }
    
        private async Task DoMyStuff(int newIndex)
        {
            // do stuff
        }
    }
    

    The extension just does the same logic but abstracts it and the state management away so you don't have to crud up your control with private fields to hold it. Hope this helps.