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?