将 Blazor RenderFragment 参数转发给子组件会引发异常

Posted

技术标签:

【中文标题】将 Blazor RenderFragment 参数转发给子组件会引发异常【英文标题】:Forwarding Blazor RenderFragment parameter to child components throws exception 【发布时间】:2021-05-29 11:30:12 【问题描述】:

更新:Microsoft 已确认此问题。 (Github issue),并且可能会在 .Net 6 (github issue) 中修复/解决。


我可以使用 RenderFragment 将(子)内容传递给组件。这可以是可选的。采取:

@*Baz.razor*@
@if (Bar is null)

    <div>nobar</div>

else

    <div>Render bar: @Bar</div>


@code 
    [Parameter]
    public RenderFragment Bar  get; set; 

我可以的

@*FooPage.razor*@
@page "/foopage"
<Baz />

呈现“nobar”,或

@*FooPage.razor*@
@page "/foopage"
<Baz Bar=@bar/>

@code 
    private RenderFragment bar = (builder) =>  builder.AddMarkupContent(0, "<div>bar</div>"); ;

呈现“Render bar: bar”(由于 div 带有换行符)

所以,现在我遇到了一个新情况,其中有一个介于两者之间的组件,称为 Foo

@*Foo.razor*@
<Baz Bar=@Bar />

@code 
    [Parameter]
    public RenderFragment Bar  get; set; 

我将 FooPage.razor 修改为

@*FooPage.razor*@
@page "/foopage"
<Foo />

即我正在将Bar 参数从Foo 转发到Baz... 这不起作用并且给了我一个相当长的错误(在这个问题的底部)。 (注意:当我在 FooPage 中分配 Bar 时,它确实有效。)

这对我来说很奇怪。我可以检查Bar 是否为空,并使用它。但是,我不能将Bar 显式设置为null,或者传递一个null 引用。

我也直接尝试过这样做:

@*FooPage.razor*@
@page "/foopage"
<Foo Bar=@nullrf />

@code 
    private RenderFragment nullrf;

这也会引发同样的异常。

即如果你把它排除在外,它就是空的,但是如果你将它分配为空,它就会中断。这是一个错误吗?

ArgumentException: Delegate to an instance method cannot have null 'this'.
System.MulticastDelegate.ThrowNullThisInDelegateToInstance()
System.MulticastDelegate.CtorClosed(object target, IntPtr methodPtr)
BlazorServerDefault.Pages.Foo.BuildRenderTree(RenderTreeBuilder __builder)
Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.Rendering.htmlRenderer.HandleException(Exception exception)
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(int componentId, RenderFragment renderFragment)
Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.HandleException(Exception exception)
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToPendingTasks(Task task)
Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(int componentId, ParameterView initialParameters)
Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.CreateInitialRenderAsync(Type componentType, ParameterView initialParameters)
Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.RenderComponentAsync(Type componentType, ParameterView initialParameters)
Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext+<>c__11<TResult>+<<InvokeAsync>b__11_0>d.MoveNext()
Microsoft.AspNetCore.Mvc.ViewFeatures.StaticComponentRenderer.PrerenderComponentAsync(ParameterView parameters, HttpContext httpContext, Type componentType)
Microsoft.AspNetCore.Mvc.ViewFeatures.ComponentRenderer.PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
Microsoft.AspNetCore.Mvc.ViewFeatures.ComponentRenderer.RenderComponentAsync(ViewContext viewContext, Type componentType, RenderMode renderMode, object parameters)
Microsoft.AspNetCore.Mvc.TagHelpers.ComponentTagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.<RunAsync>g__Awaited|0_0(Task task, TagHelperExecutionContext executionContext, int i, int count)
BlazorServerDefault.Pages.Pages__Host.<ExecuteAsync>b__14_1() in _Host.cshtml
+
    <component type="typeof(App)" render-mode="ServerPrerendered" />
Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext.SetOutputContentAsync()
BlazorServerDefault.Pages.Pages__Host.ExecuteAsync() in _Host.cshtml
+
    Layout = null;
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, bool invokeViewStarts)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

【问题讨论】:

> 这对我来说很奇怪。对我来说不是......根据我自己的经验,如果你想创建一个带有可选页脚模板的模板化组件,你不会为参数属性分配空值,而是跳过添加它,然后检查是否参数属性是否为空。当您检查参数属性的值是否为 null 时,您实际上是在检查一个值的存在,而不是它的内在价值,无论它是什么。 @enet,是的,但似乎没有办法有条件地添加参数。基于某种状态,我希望它存在或不存在。 SO上还有一些类似的问题,但没有好的答案。 仍然无法转发参数。请注意,在现实生活中,有许多参数,因此为每个参数制作条件语句将增加 O(N!)。 真正应该问的问题是为什么要转发一个空值的参数。检查条件是程序员的日常工作。您是否在触发事件处理程序委托之前做出条件语句?那是一样的。同样,为什么要传递一个空值的委托?你不能用它做任何事情,所以不要通过它。 @enet 我不想要传递一个空值的委托。但是,Blazor 的设计者选择以这种方式实现 RenderFragment。我想要做的是模板子内容并传递该内容。 RenderFragment 似乎是要走的路,我在很多页面上都找到了。 【参考方案1】:

当您尝试将Bar 作为属性传递时,Razor 编译器将尝试将其呈现为 Foo 组件的一部分,这不是您想要的,也是当 Bar 为空时它会失败的原因。

如果实际用例适合这一点,您可以通过属性喷溅来实现您的目标。

@*Foo.razor*@
<Baz @attributes=PassThrough/>

@code 
    [Parameter]
    public RenderFragment Bar  get; set; 
    Dictionary<string,object> PassThrough;
    protected override void OnInitialized()
    
        PassThrough = new Dictionary<string,object>();
        PassThrough.Add(nameof(Baz.Bar),this.Bar);
    

编辑:在这里测试:Blazor Repl

【讨论】:

谢谢。我希望这不是唯一/最好的解决方案。我给出的例子被大大简化了。在我们想要移植(从表单)的实际项目中,会有一个完整的组件层次结构,每个组件都有几个子组件。必须分别跟踪每个组件的所有“传递”参数将使代码变脏且难以维护...... 我想我已经对“当您尝试将 Bar 作为属性传递时,Razor 编译器将尝试将其渲染为 Foo 组件的一部分”......第一件事:它不是一个属性,它是一个参数,对吧? “属性”是一个 HTML 的东西。但是 Blazor 组件实际上是 C# 类。 Bar 是一个属性,它具有 ParameterAtrribute。 Sow当我说&lt;Baz Bar=@Bar /&gt; 时,我希望编译器生成var barObj = new Baz Bar=Bar ;。如果你通过它,为什么它会尝试渲染它?这没有任何意义。 好吧,我不是在这里争论属性,这是一个模糊的领域——但参数的值是通过 razor 和渲染树中的属性设置的。我相信总有其他方法可以做事,你绝对可以称这是一个错误并记录一个问题,因为很容易争论以这种方式传递的渲染片段应该只是通过。 我自己的经验是这样的——一旦你开始遇到这样的问题,这是一个很好的迹象,表明你的设计有问题——我并不是说肯定有,但现在是采取退一步检查一下——像这样直接传递渲染片段感觉像是设计中的一个潜在错误——但我完全接受我没有足够的关于你的项目的详细信息来确定:) 谢谢。我目前在一个大项目中,该项目已经过时并且有很多意大利面条代码。他们使用某种“表单生成器”项目,该项目提供了大量的组件(不同类型然后是剃刀)模板。这些被转换为数据库条目,每次更新都需要重新生成。它很难维护,也很容易搞砸。我试图向我的团队展示我们可以使用 Blazor/Razor 组件替换它,这将使生活变得更加轻松。但是如果我们不能有层次结构,传递像 RenderFragments 这样的东西,我将很难卖掉它......【参考方案2】:

这不是真正的答案,但我需要在某个地方放置一些比注释更好的代码。

问题在于在为 Razor 组件构建适当的类时,将 razor 代码预编译到 BuildRenderTree 方法中的方式。责怪组件制造商。您需要以 Razor 期望的方式声明 RenderFragments。

您的编码 - 使用属性方法:

<Baz Bar="@Bar"></Baz>

Razor 构建器将其变成:

    public partial class Foo2 : Microsoft.AspNetCore.Components.ComponentBase
    
        #pragma warning disable 1998
        protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        
            __builder.OpenComponent<TestBlazorServer.Components.Bar>(0);
            __builder.AddAttribute(1, "Bar", 
#nullable restore
          Bar

#line default
#line hidden
#nullable disable
            );
            __builder.CloseComponent();
        
        #pragma warning restore 1998
#nullable restore
#line 3        
    [Parameter]
    public RenderFragment Bar  get; set; 

请注意,Bar 是作为属性添加的,如上面的答案中所述。

现在如果你这样声明:

<Baz>
 <Bar>@Bar</Bar>
</Baz>

Bar 被正确添加为 RenderFragment。

    public partial class Foo : Microsoft.AspNetCore.Components.ComponentBase
    
        #pragma warning disable 1998
        protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        
            __builder.OpenComponent<TestBlazorServer.Components.Bar>(0);
            __builder.AddAttribute(1, "Baz", (Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => 
                __builder2.AddContent(2, 
#nullable restore
#line 5          
Bar

#line default
#line hidden
#nullable disable
                );
            
            ));
            __builder.CloseComponent();
        
        #pragma warning restore 1998
#nullable restore
#line 9        
    [Parameter]
    public RenderFragment Bar  get; set; 

这是我几个月前在代码中构建组件的后端。无论如何,我不确定你是否可以用这种方式构建你的东西​​,但它解释了你所看到的。

【讨论】:

谢谢!我会检查这个__builder.OpenComponent&lt;TestBlazorServer.Components.Bar&gt;(0); 这是一个错字吗?即应该是__builder.OpenComponent&lt;TestBlazorServer.Components.Baz&gt;(0);(甚至是Foo?)没有组件Bar 另外,第一个代码实际上似乎可以正常工作,as I discovered 我有一个名为 Bar 的组件:当我在测试站点中构建组件时,组件名称中有错字!我假设您知道在哪里可以找到剃刀生成的代码? 不,我没有,而且在谷歌上似乎不太容易找到。你能指点我(或一些文档)吗? 在项目obj文件夹中探索。 ..\SPA\obj\Debug\net5.0\Razor 是我的 SPA 项目库的 razor 构建类文件的路径。每个文件都是 xxx.razor.g.cs。

以上是关于将 Blazor RenderFragment 参数转发给子组件会引发异常的主要内容,如果未能解决你的问题,请参考以下文章

如何在 RenderFragment Antd Blazor 中触发 @onclick

Blazor 服务端组件 Render, RenderFragment ,RenderTreeBuilder, CascadingValue/CascadingParameter

blazor return RenderFragment '__builder' 在当前上下文中不存在

Blazor中的无状态组件

Blazor University (18)使用 RenderFragments 模板化组件 —— 创建 TabControl

模板化 Blazor 组件