The Practical Client

Two 'Gotchas' (and Fixes) for Blazor in .NET Core 3.0

The release version of Blazor contains two surprising changes (surprising, at least, to Peter) -- changes that broke some of his code. Here are both of those "gotchas" with the workarounds that he implemented.

An official, production-ready version of Blazor dropped on Sept. 23 ... with two "gotchas" from previous versions (to be fair, these issues were in the final two releases before the 23rd: Preview 9 and RC1). I've got a fairly extensive Blazor codebase lying around at this point so, based on my testing, these are the only two problems that I've run into that have actually broken my code (one of the reasons I'm so confident is that I've written a course on Blazor for Learning Tree International and there's nothing like writing a course to force you to be thorough).

The first issue is at a pretty high level: You can no longer pass parameters from the View or Razor Page hosting your component to the component (there is one exception, as I'll discuss below). The second issue is more focused: If you attempt use HTML embedded inside a component's method as part of your component's UI, your code won't compile.

Here are those problems in more depth and solutions for both.

Passing Parameters
In the release version of Blazor, you can use some variation on this code to invoke your components from an MVC View, a Razor Page, or a View Component:

@(await Html.RenderComponentAsync<CustomerUpdate>(RenderMode.ServerPrerendered))

There are three RenderMode options you can pass to RenderComponentAsync: ServerPrerendered, Server, and Static. You can only pass a parameter to the component through the RenderComponentAsync method if you use the Static option. This code, for example, passes the View's Model property to the component through a parameter called cust:

@(await Html.RenderComponentAsync<CustomerUpdate>(RenderMode.Static, new { cust = Model }))

Unfortunately, if you use the Static option, your component stops firing events. This means your component can't have any interactivity (your buttons stop firing click events, for example). For any real application, static components aren't an option. And, in many cases, I not only want in interactive component, I want one that integrates with my server-side application by accepting data from the host View or Razor Page.

There is a relatively straightforward two-part workaround for this problem:

  • Embed your View or Razor Page's data in a JavaScript function that returns your data
  • Call that function from your component

I could, for example, add this script element to the page that hosts my component to define a JavaScript function that returns one of the properties on my Model object:

<script>
  function GetCustomerId() {
    return "@Model.custId";
  }
</script>

Then, in the component invoked from this page, I can call that function and catch the custId that it returns. The first step in calling this JavaScript function is to grab Blazor's JavaScriptRuntime using Blazor's inject directive. This code does the trick, stuffing the runtime into a field called jrt:

@inject IJSRuntime jrt

Now, in my component's code section, I can call the JavaScript function using the runtime's InvokeAsync method. While I want to retrieve that data as early as possible in my component's lifecycle, the JavaScript runtime can't be used earlier than my component's OnAfterRender method. As a result, I'd add code like this to my component's code section, specifying the type of data I expect to retrieve when I call the InvokeAsync method:

string custId;
protected async override void OnAfterRender(bool firstRender)
{
  if (firstRender)
  {
    custId = await jrt.InvokeAsync<string>("GetModel");
  }
}

While this solution works, even I have to admit that it's awkward. To begin with, I can't return an object from my JavaScript function without serializing it to JSON first (not the end of the world but, still ... ). Second, while this example is relatively terse, I am embedding my server-side data into the HTML page being sent down to the user. That could get bulky and might also cause me to expose data on the client that I'd prefer to keep on the server (encryption could deal with that ... at the expense of making my code even more awkward).

Still it works, even if I want to integrate routing into my application. If my Router is in a component called App, I can start up my application with code like this and my component will still be able to call my JavaScript function:

@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

Hopefully, this problem is a temporary situation. The Blazor team has already indicated that changes in server-side prerendering are possible in upcoming releases.

Integrating HTML and Functions
Since Preview 6, you've had the ability to integrate HTML into your C# methods and use those methods to build your UI. For example, while Blazor's InputSelect component will add a <select> element to my UI, it's my responsibility to build the options list to go with it. I can build that list in my component's UI with code like this:

Product: <InputSelect @bind-Value="orderDetail.ProductId">
               @{foreach (Product prod in prods)
               {
                <option value="@prod.Id">@prod.Description</option>
               }
              }
         </InputSelect>

And there's nothing wrong with that. However, I'd rather keep code out of my UI so I'd prefer to move that code into a method in my component's code section. I'd prefer, in other words, to put that code in a method like this:

private void BuildOptions()
{
  foreach (Product prod in prods)
  {
    <option value="@prod.Id">@prod.Description</option>
  }
}

With that refactoring complete, since Preview 6 I've been able to call my method from my UI like this:

Product: <InputSelect @bind-Value="orderDetail.ProductId">
           @{ BuildOptions(); }
         </InputSelect>

In the release version of Blazor, however, this code generated a set of compile-time error messages about an undeclared field name __builder (that's with two underscores in front). If you look at the code that's generated from your cshtml file, it's pretty easy to see why: The generated code for the method includes a reference to a __builder field holding a Blazor RenderTreeBuilder ... but the field's scope makes it invisible to your method. The __builder field is, on the other hand, accessible to code in your component's UI/markup.

It's pretty obvious to me that this is a bug and, as such, will probably be fixed in an upcoming release. In the meantime, however, there is a workaround and it just requires three steps.

The first step is to add a using statement to support the object stuffed into the __builder field (this isn't strictly necessary but if you omit this, your code will get uglier later on):

@using Microsoft.AspNetCore.Components.Rendering 

Second, in your markup you must alter the call to your method to pass the __builder field it needs as a parameter. My revised InputSelect code would look like this after that change:

Product: <InputSelect @bind-Value="orderDetail.ProductId">
                @{ BuildOptions(__builder); }
         </InputSelect>

The final step is to accept that parameter in your method, like this (if you omit the using statement I recommended earlier then you'll have to tack the namespace onto the front of this parameter's type):

private void BuildOptions(RenderTreeBuilder __builder)
{

There you go: two solutions for the only two surprises that I've found in the release version of Blazor. Plus, there's a very real possibility that both these issues may disappear before Christmas. Now that would be a nice present.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.

Upcoming Events