Closed jimitndiaye closed 2 years ago
Thanks @jimitndiaye. We'll investigate further and get back to you.
@egil Thank you. I'd like to add that doing the exact same component described above but in razor works. The following razor syntax is functionally equivalent to to what's going on in the component above:
<ErrorBoundary>
<ChildContent>
<DynamicComponent @ref="_dynamicComponent" Type="ComponentType" Parameters="ComponentParameters"/>
</ChildContent>
<ErrorContent>
<MudStack Class="border-2 mud-border-error" AlignItems="AlignItems.Center" Justify="Justify.Center" Style="min-height: 50px">
<MudAlert Severity="@Severity.Error">An internal UI error occurred. Please contact support.</MudAlert>
</MudStack>
</ErrorContent>
</ErrorBoundary>
If i run the same test but with this razor component it works, even though they are functionally doing the exact same thing - both output the exact same markup/component tree.
Hmmm I might have a hunch what is causing the problem:
builder.AddAttribute(1, nameof(ErrorBoundary.ChildContent), (RenderFragment)(childContentBuilder =>
{
childContentBuilder.OpenComponent(2, Type);
If Type == MudTextField<string>
wouldn't be the produced markup something like:
<ErrorBoundary ChildContent="MudTextField<string>"
which is basically invalid XML.
Just on top of my head did you really want to add builder.AddAttribute
in this case.
You wanted to have something like this @jimitndiaye , or?
<ErrorBoundary>
<ChildContent>
// Here your stuff
</ChildContent>
</ErrorBoundary>
Wait, the razor version works?
That's surprising. Have you looked at the code the Blazor/Razor compiler generates from the razor version to make sure they are 100% the same?
@linkdotnet your interpretation of that line of code isn't quite accurate. The code sets the ChildContent
property to a RenderFragment
, not Type
. Like I said, the code actually works at runtime and renders the component indicated (in the test it is a MudTextField)
The equivalent razor would be
<ErrorBoundary>
<ChildContent>
<MudTextField T="string" />
</ChildContent>
</ErrorBoundary>
@egil I mean in blazor (WASM/Server) the razor version works and they output the exact same code (in fact i originally did it in razor then copied/tweaked the generated render code to suit my needs)
@egil The razor version I posted above uses the built-in DynamicComponent and outputs the following render code:
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.OpenComponent<Microsoft.AspNetCore.Components.Web.ErrorBoundary>(0);
__builder.AddAttribute(1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
// this part is where it differs from the original component in the issue. instead of using DynamicComponent I just directly render the component (using the same code that DynamicComponent does)
__builder2.OpenComponent<Microsoft.AspNetCore.Components.DynamicComponent>(2);
__builder2.AddAttribute(3, "Type", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<System.Type>(
ComponentType
));
__builder2.AddAttribute(4, "Parameters", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<System.Collections.Generic.IDictionary<System.String, System.Object>>(
ComponentParameters
));
__builder2.AddComponentReferenceCapture(5, (__value) => {
_dynamicComponent = (Microsoft.AspNetCore.Components.DynamicComponent)__value;
}
);
__builder2.CloseComponent();
}
));
__builder.AddAttribute(6, "ErrorContent", (Microsoft.AspNetCore.Components.RenderFragment<System.Exception>)((context) => (__builder2) => {
__builder2.OpenComponent<MudBlazor.MudStack>(7);
__builder2.AddAttribute(8, "Class", "border-2 mud-border-error");
__builder2.AddAttribute(9, "AlignItems", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<MudBlazor.AlignItems?>(
#nullable restore
AlignItems.Center
));
__builder2.AddAttribute(10, "Justify", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<MudBlazor.Justify?>(
Justify.Center
));
__builder2.AddAttribute(11, "Style", "min-height: 50px");
__builder2.AddAttribute(12, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder3) => {
__builder3.OpenComponent<MudBlazor.MudAlert>(13);
__builder3.AddAttribute(14, "Severity", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<MudBlazor.Severity>(
Severity.Error
));
__builder3.AddAttribute(15, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder4) => {
__builder4.AddContent(16, "An internal UI error occurred. Please contact support.");
}
));
__builder3.CloseComponent();
}
));
__builder2.CloseComponent();
}
));
__builder.CloseComponent();
}
#pragma warning restore 1998
@egil I mean in blazor (WASM/Server) the razor version works and they output the exact same code (in fact i originally did it in razor then copied/tweaked the generated render code to suit my needs)
Ah so the razor version does not work in bUnit, but works in Blazor?
@egil no the razor version works in both. It's the code version that works in Blazor but not in bUnit. Even though they are functionally identical. The only difference is what's happening in the ErrorBoundary
s ChildContent
property - The razor version uses the built-in DynamicComponent
to render the component while the code version just directly renders the component without going through DynamicComponent
(instead just pretty much copies the code that dynamic component uses to render the component). That's what's happening here (from the original component above):
builder.AddAttribute(1, nameof(ErrorBoundary.ChildContent), (RenderFragment)(childContentBuilder =>
{
childContentBuilder.OpenComponent(2, Type);
if (Parameters is not null)
foreach (var entry in Parameters)
builder.AddAttribute(3, entry.Key, entry.Value);
childContentBuilder.AddComponentReferenceCapture(4, component =>
{
Instance = component;
});
childContentBuilder.CloseComponent();
}
));
Like I mentioned on Gitter, much of the code in Htmlizer is lifted from Blazors prerenderer logic, but we might need to upgrade to a newer version.
Yea I just found it interesting that functionally identical code behaves differently in bUnit. If you run either version (code or razor) in Blazor you'd see the the output is identical. Maybe upgrading the Htmlizer is the answer.
Yea I just found it interesting that functionally identical code behaves differently in bUnit. If you run either version (code or razor) in Blazor you'd see the the output is identical.
Me too. Just sharing that detail from our Gitter conversation with Steven.
But we also need to make sure the two are actually functionally the same, or rather, they do not seem to be, since they behave differently, which by definition means they are not. But it's interesting none the less.
Btw. Here is the original code for DynamicComponent. https://source.dot.net/#Microsoft.AspNetCore.Components/DynamicComponent.cs
@egil yea I borrow quite a lot of code from that in the above component.
The code inside this RenderFragment delegate is pretty much a lift from the Render function in DynamicComponent:
builder.AddAttribute(1, nameof(ErrorBoundary.ChildContent), (RenderFragment)(childContentBuilder =>
{
childContentBuilder.OpenComponent(2, Type);
if (Parameters is not null)
foreach (var entry in Parameters)
builder.AddAttribute(3, entry.Key, entry.Value);
childContentBuilder.AddComponentReferenceCapture(4, component =>
{
Instance = component;
});
childContentBuilder.CloseComponent();
}
));
And that part is the only difference between the code version and the razor version - the code version just in-line's the Render function from DynamicComponent while the razor version just calls DynamicComponent to do it instead.
You get the error in that section where it's checking if Parameters is null. If not null you get the error. If null or empty you don't get the error.
For future reference the current version of the HtmlRenderer in the aspnetcore repo: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs
there are some smaller changes since the original version. On first sight nothing exciting. Will have a look tomorrow. We also made changes on our own, even though I don’t think they changed anything in the behaviour.
@linkdotnet we also need to make sure we understand what the difference IS between the razor version @jimitndiaye posted and the handcoded C# version, because the first one does work with bUnit.
It could explain why, and also provide a hit as to how to fix this.
Once we know more, we might also reach out to the Blazor team to verify this is actually legal/expected behavior that @jimitndiaye is seeing.
I found the issue. Very easy to miss (took me almost 2 hours including diffs between our HtmlRenderer and the one in the aspnetcore repo):
void Render(RenderTreeBuilder builder)
{
builder.OpenComponent<ErrorBoundary>(0);
builder.AddAttribute(1, nameof(ErrorBoundary.ChildContent), (RenderFragment)(childContentBuilder =>
{
childContentBuilder.OpenComponent(2, Type);
if (Parameters is not null)
foreach (var entry in Parameters)
// This should be childContentBuilder and not builder
// So the super easy fix, which lets the test pass
// childContentBuilder.AddAttribute(3, entry.Key, entry.Value);
builder.AddAttribute(3, entry.Key, entry.Value);
The reason is simple: You use the wrong object / builder ;)
Using builder
instead of childContentBuilder
inside your lambda will for sure result in invalid HTML ;)
I would close the issue for now. If you have further questions let us know.
@linkdotnet good catch! Can't believe I missed that.
Describe the bug When rendering a component via bUnit I get an error but when rendering in Blazor (both Server and WASM) the same component works fine.
Example: A stripped down (only dependency is MudBlazor) version of the component is defined below:
With this test:
Results in this output:
Expected behavior:
The test should pass without error.
Version info:
Additional context: