【问题标题】:How can I somehow combine EventCallback<T>?我怎样才能以某种方式结合 EventCallback<T>?
【发布时间】:2022-10-08 00:42:48
【问题描述】:

所以我发现自己需要对一些 Blazor 组件进行子类化,其中一个原因是我需要从本质上创建一个装饰器来扩展它的功能。该扩展的一部分是附加一些额外的事件处理。由于组件现在使用EventCallback,它不是委托类型,因此我不能像使用多播委托那样简单地向它添加另一个处理程序。我可以替换它,但这意味着该组件的任何消费者都无法注册他们自己的任何处理程序,因为我的会覆盖它,所以现在我正在尝试包装它。这是场景的伪表示以及我正在尝试做的事情

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);
    }
}

这里的想法是,我只是通过在SetParametersAsync() 中解决这个问题来确保用户的处理程序已被绑定,以便我可以将它包装在一个新的回调中,该回调将首先调用他们的处理程序,然后再运行我的处理程序。但是由于它是具有由基类调用的属性的基组件,这意味着我需要用我的新处理程序替换该特定属性,但这样做,这意味着新处理程序正在调用实际上是旧处理程序现在是新的处理程序,所以它现在是一个无限递归调用堆栈,并导致堆栈溢出。

所以我的第一个想法是,如果我能以某种方式获得原始 EventCallback 的副本,或者至少提取它的委托以便我可以创建一个新的回调,那么它就不会再引用自己了(因为它是一个结构而感到困惑,我想它总是自然地是副本),但我找不到任何方法来做到这一点。我尝试只使用EventCallback.Factory.Create(this, SomethingChanged),希望它会使用相同的委托创建一个全新的回调实例,但它没有改变任何东西;同样的结果。

如果我可以覆盖原始组件的 SomeInternalProcess() 方法,这样我就可以在调用基本方法之前或之后将我的进程插入到那里,这当然不是问题,但它是第 3 方库。或者,如果 SomethingChanged 属性本身是虚拟的,我可以覆盖它以拦截它的设置器,但事实并非如此。

所以简而言之,有没有办法实现与多播委托相同的效果,以便我可以保留任何已注册的处理程序但与我自己的结合?或者至少有某种方式取消引用原始的 EventCallback 或提取它的委托,以便我可以创建一个新的委托?

例如

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

更新1:

我尝试通过在子类上声明我自己的来“隐藏”SomethingChanged 事件回调,以便我可以在基础上注册我自己的处理程序,其中除了我自己的处理程序之外还包括用户的处理程序。这在标准 C# 测试中有效,但 Blazor 不喜欢它。它在渲染期间将其视为重复属性并引发异常。

更新 2:

哈卡罗尼。 EventCallbackEventCallback&lt;T&gt; 都将委托存储在名为 Delegate 的内部字段中。为了看看它是否可以工作,我通过反射将它取出并使用它来创建一个新的 EventCallback 来替换用户创建的 EventCallback,它将我们两个包裹在一起,先执行他们的然后我的。它有效,而且我还没有看到任何奇怪的副作用。但我讨厌它,原因很明显。但这让我想知道,也许我所需要的只是让微软公开那个领域。我敢肯定它存在某种风险,但它只是一个函数指针。只要它是只读的,应该没问题吧?

【问题讨论】:

  • 我将通过在我的子类中声明我自己来尝试简单地隐藏“SomethingChanged”。因此,我会将我的处理程序添加到基础,然后在我的处理程序中,我将调用客户端处理程序,因为它们应该是分开的。感觉。所以。错误的。尽管....
  • Welp 在标准 c# 中有效,但 Blazor 不喜欢它,因为它将其视为重复属性并抛出。
  • 除了OriginalBlazorComponent 实例的父组件之外,还有谁在尝试使用SomethingChanged?如何?
  • @MrCakaShaunCurtis 设计为“未知”。它是一个组件,你不知道谁会使用它。据您所知,它可能是世界上任何其他开发人员。原始组件的设计公开了该事件回调,因此该控件的用户可以订阅并使用它做任何他们想做的事情(例如,当某些内容更改选项卡索引时,我还想更新此值以便其他控件可以做出反应)。我只需要在其中插入一些其他东西,而不会破坏他们仍然使用该界面的能力。
  • 请参阅我更新的答案以及对您的答案的一些修改。

标签: .net-core blazor multicastdelegate


【解决方案1】:

好的,一个简单的责任链模式实际上起作用了:

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);
    }
}

这只是将用户/开发人员可能已在私有字段中定义的任何回调存储,然后用我的回调替换它,该回调将调用他们然后我的。您必须先调用设置参数,以便它们的回调绑定,否则它最终会覆盖您的。然后最后,调用 base 带有一个空的参数视图来指示参数已经设置(这是框架内的一个模式,不这样做会导致异常)。

现在从剃刀的角度,从用户/开发者透视,而不是像这样使用原始组件:

<OriginalBlazorComponent SomethingChanged="MyChangeHandler" />

他们可以使用带有扩展功能的装饰组件

<MySubclassedComponent SomethingChanged="MyChangeHandler" />

这样做并没有替换他们的处理程序,它只是确保我的处理程序也被调用。而且,从基础组件(第三方原始组件)的角度来看,它只是正常运行,就像子类甚至不存在一样。

归根结底,EventCallback 是我们在 Blazor 中使用的,但显然它不能像我们可以与委托一样与其他回调结合使用(据我所知,无论如何),否则这将非常简单;我只需将我的处理程序添加到委托并完成它。如果有人知道这样做的方法,我真的很想知道。

然后为方便起见,将其重构为扩展:

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);
            });
        }
    }
}

因此,这为将来简化了它,并将像这样使用:

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
    }
}

该扩展只执行相同的逻辑,但将其和状态管理抽象化,因此您不必使用私有字段来控制您的控件来保存它。希望这可以帮助。

【讨论】:

  • 实际上回想起来,我可能已经能够绑定它,然后将其移动到一个私有字段并用我的处理程序替换SomethingChanged,该处理程序将调用该私有字段(他们的)然后运行我的......远没有那么有趣但不那么复杂和没有反射。我以后得试试
  • 是的……那行得通。将更新此答案。
  • It's a decorator pattern. 我不认为这是装饰器模式。它只是子类化。 C# 支持装饰器模式,但我不确定您目前是否可以在 Blazor 中实现它,因为虽然 Blazor 组件是一个类对象,但由于呈现功能,它的实例化是特殊的。无论如何,您在这里所做的是子类化。我会投票给您,因为您的回答为这里的开发人员提供了一些如何克服 Blazor 限制的想法。
  • @enet 它不是教科书装饰器,因为它不会获取并保存对原始代理调用的引用。我最初是这样处理设计的,但就您而言,因为实例化的工作方式不同,正确实现它会非常尴尬。所以是的,它主要只是子类化,但采取的方法是维护原始接口,同时添加额外的职责并保持在同一类对象中,这是该模式的主要目标。区别在于我猜是类而不是装饰的对象。
  • 所以也许是“子类化、装饰器和 CoR 思想模式的某种混合”。不会完全从舌头上滚下来。
【解决方案2】:

你可以(应该)只做 SomeInternalProcess virtual

或者在对 SomethingChanged() 的调用周围添加一个虚拟包装器:

public class MySubclassedComponent : OriginalBlazorComponent
{
   protected override OnSomethingChanged(int i)
   {
      // ignore, replace or augment the base class behaviour
   }
} 

【讨论】:

  • 我在帖子中提到我实际上正在寻找它,但该方法不是虚拟的,不能被覆盖,而且它是一个 3rd 方库,所以我不能改变它:)。实际上有点烦人,因为这正是我在同一个库中解决不同组件的方法。
  • 然后也许考虑组合而不是继承。
  • 你不能像那样在 blazor 中真正组合组件,因为类本身就是你用来渲染的元素。你最终不得不重新实现整个事情,这完全违背了这一点。
【解决方案3】:

更新的答案

我努力想清楚你想要做什么。但是,有了您的答案和 cmets,这是我的摘要和一些代码。

您有一个Callback,您希望确保始终调用基本方法,即使是从子级调用时也是如此。

我认为您的回答使您尝试做的事情过于复杂。为了记录,这是我的版本:

public class MyBaseComponent : ComponentBase
{
    private EventCallback<string> _childSomethingChanged;
    private Func<string, Task> BaseSomethingChanged;

    [Parameter] public EventCallback<string> SomethingChanged { get; set; }

    public MyBaseComponent()
    {
        // Cache the delegate
        BaseSomethingChanged = (value) =>
        {
            return MustRunProcess(value);
        };
    }

    private async Task MustRunProcess(string value)
    {
        // run whatever processes you need in here
        await _childSomethingChanged.InvokeAsync("Updated by Base");
    }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);

        if (this.SomethingChanged.HasDelegate)
            _childSomethingChanged = SomethingChanged;

        // This is a little naughty 
        // you shouldn't really update a parameter set value but needs must 
        SomethingChanged = EventCallback.Factory.Create( this, this.BaseSomethingChanged);

        return base.SetParametersAsync(ParameterView.Empty);
    }
}

这是子组件:

@inherits MyBaseComponent

<h3>MyComponent</h3>

<div class="alert alert-primary">
    @this.ChildData
</div>

<div>
    <button class="btn btn-primary" @onclick=DoUpdate>Update</button>
</div>
@code {
    private string ChildData = string.Empty;

    private void DoUpdate()
    {
        this.ChildData = DateTime.Now.ToLongTimeString();
        this.SomethingChanged.InvokeAsync("Updated by Child");
    }
}

和一个测试页:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

<MyComponent SomethingChanged=DoSomething />

<div class="alert alert-dark mt-2">
    @this.dataValue
</div>


@code {
    private string dataValue = string.Empty;

    private void DoSomething(string value)
        => this.dataValue = value;
}

【讨论】:

  • 我认为您并没有完全理解我在问题中所说明的内容;这是一个装饰器模式。您的示例显示了原始组件,我们没有尝试使用原始组件,我们正在尝试对其进行子类化,以便子组件是相同的组件(相同的外观,相同的功能)但具有注入的功能(装饰)所以我每次我想使用它时都不必一直连接相同的功能。我不想通知其他组件,我想扩展现有组件。
  • NP :-) - 经常发生在这里!我已经看到你的答案,所以了解你现在的情况。我将删除此答案,稍后再详细查看您的答案。
【解决方案4】:

我实际上需要为向导做类似的事情,我的解决方案是delegate EventHandlereventEventCallback 的组合。

基类:

public class MyComponentBaseClass : ComponentBase
{
    [Parameter]
    [EditorRequired]
    public EventCallback OnClick { get; set; }

    public delegate Task OnClickInternalEventHandler();

    public event OnClickInternalEventHandler OnClickInternalEvent;

    public async Task ComponentClickMethod()
    {
        if (OnClick.HasDelegate)
            await OnClick.InvokeAsync();

        //Do the things

        await OnClickInternalEvent();
    }
}

父组件:

@inherits MyComponentBaseClass

<div ChildContainer>
    <CascadingValue Value="(MyComponentBaseClass)this">
        @ChildContent
    </CascadingValue>
</div>

@code {
    [Parameter]
    [EditorRequired]
    public RenderFragment ChildContent { get; set; }
}

子组件:

<button @onclick="ParentBase.ComponentClickMethod">Execute Event on Parent</button>

@code {
    [CascadingParameter]
    public MyComponentBaseClass ParentBase { get; set; }

    protected override void OnInitialized()
    {
        ParentBase.OnClickInternalEvent += PrivateChildMethod;
    }

    async Task PrivateChildMethod() =>
        //Your code here
        Console.WriteLine("Private Child Method");
}

这些代表用途广泛,可以以多种不同方式使用。上面的示例不是我在向导上实现的方式,但应该是一个很好且清晰的示例。

【讨论】:

  • 回想起来,我本可以使用更好的命名。
  • 有趣,但我认为这是一个不同的场景,您实际上拥有基本组件并可以以这种方式进行设置。在我的场景中,基本组件是我无法修改的第 3 方库,所以无论他们的实现是什么,我都坚持工作。
猜你喜欢
  • 1970-01-01
  • 2012-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-13
  • 2020-01-03
相关资源
最近更新 更多