The Practical Client

How to Dynamically Build the UI in Blazor Components

You have two tools for generating your initial UI in a Blazor component: ASP.NET's Razor and Blazor's RenderFragment. Here's how to use both to integrate with your C# code (and a warning about what you can't do).

You can, using familiar Razor tools when creating a View (or page), dynamically build your component's UI. Alternatively, you can also use the rendering tools built into Blazor to dynamically construct the UI that makes up your component at startup. I'm going to show how both of those options work in this column

That's not the same as manipulating your component's HTML as your component executes. For that you can use binding, buy a third-party component, or call out to jQuery through Blazor's JavaScript interop. But if you want to create an initial UI dynamically, here's how you'll do it.

As my case study I'll use an (admittedly, contrived) View that contains multiple forms. In this case study, the Model object that's passed to this View contains an ArrayList of objects for a single customer. The ArrayList can contain any combination of different "customer related" objects: the customer's profile object, the customer's address object , the customer's billing plan and so on. In this View, we'll set up each object with a different form and each form will have a button that invokes a different C# method to handle processing that form.

Leveraging Razor
One way to generate this page is to use "good old Razor" code inside your Blazor component. That Razor code might look something like this (to simplify this code I'm using partial pages to pull in the HTML for each of the forms):

@foreach (object obj in Model)
{
   <form>
      if (obj is CustomerProfile)
      {
         <partial name="Shared/_CustProfile.cshtml" ... 
         <input type="button" @onclick="@SendProfileData" value="Update" />
      }    
      if (obj is CustomerAddress)
      {
         <partial name="Shared/_CustProfile.cshtml" ... 
         <input type="button" @onclick="@SendAddressData" value="Update" />
      }
      if (obj is CustomerBillingPlan)
      {
         <partial name="Shared/_CustProfile.cshtml" ... 
         <input type="button" @onclick="@SendBillingData" value="Update" />
      }       
   </form>
}

The code block for this component would contain all the methods required by each of the forms and look something like this:

@code {
    public void sendProfileData()
    { ... }
    public void sendAddressData()
    { ... }
    public void sendBillingData()
    { ... }

This will work and is even relatively maintainable: Adding new objects just requires adding a new block to the Razor code, a new Partial Page, and a new method to the code block. No existing code would have to be modified.

Leveraging Blazor
You can also generate these multiple forms in C#, using Blazor's RenderFragment class. This class has methods that allow you to add elements, attributes, and content to the Fragment. You can then insert the fragment into your component using Razor's @ symbol.

I'm going to wrap my code inside a method so that I could (if I wanted to) use it in several places in my page. So the first step in this process is to create a method that will return the RenderFragment that will generate my HTML:

private RenderFragment BuildForm(string notes)
{

My next step is to declare a variable to hold my RenderFragment and and set it to accept a lambda expression that defines the HTML I want to add to the page, like this:

  RenderFragment multipleForms;

  multipleForms = b =>
        {

The parameter that's passed to the lambda expression (which I've called b in this example) is a RenderTreeBuilder -- it's what does all the work when it comes to generating your HTML.

To add an element to the RenderTreeBuilder, you call its OpenElement method passing two parameters:

  • A number that represents how deeply this element is nested inside other elements
  • The name of the element

This code creates a div element that my forms will nest inside and, so, is at the first level:

b.OpenElement(1, "div");

My next step is to loop through the objects in the collection passed to my Blazor component (held in a collection called custItems) and create a form element for each object. Since I want these forms nested inside the div element I already created, I set the level to 2 when I call the OpenElement method that creates the form element:

foreach (object cust in custItems)
{
   b.OpenElement(2, "form");

Inside the form, I'll add some text to identify the form. To add content (text) to the current element, you use the RenderTreeBuilder's AddContent method. This code adds the text "Profile Data" to my form:

b.AddContent(3, "Profile Data");

I'd like to add a br element after the text so that the text will appear on a separate line. This is at the same level as the text so I use the same level number. However, in addition to opening this element I also need to close it so I call both the OpenElement and CloseElement methods (eventually, I'll have to close the div and form elements I've already opened). Here's that code:

b.OpenElement(3, "br");
b.CloseElement();

My next step is to check the type of the object and create the content unique to it. All I'll do in this column is define the button for the customer profile form. Again, because the button is at the same level as my my content and br element, I don't change the level number:

if (cust is CustomerProfile)
{
   b.OpenElement(3, "input");

My input element also needs some attributes that I can add with the RenderTreeBuilder's AddAttribute method. I can set the attributes to string values. Here's how I set the type and value attributes to specific values:

   b.AddAttribute(3, "type", "button");
   b.AddAttribute(3, "value", "Update");

I can also set an attribute's value to a member of my component. In this code, I tie the onclick attribute to a method in my component called SendProfileData (notice the absence of quotation marks around the member name):

   b.AddAttribute(3, "onclick", SendProfileData);

Finally, I have to close all of the elements that are currently open: the input, form, and div elements I created earlier (notice that CloseElement closes the "current" element so it doesn't take a level number). This is the code that does that and provides the closing brace for the lambda expression that holds all this code:

   b.CloseElement();
  }
 }
 b.CloseElement();
}
b.CloseElement();
};

The last step is to return the RenderFragment object I've created with all of this code:

return multipleForms;

To add this RenderFragment to my page, I just need to add a call to the method containing all this, somewhere in the View or Page (but above my code block). This code would do the trick:

@BuildForms()

@code {
  private RenderFragment BuildForm(string notes)
  {

Conclusions
As you can see, RenderFragment code is -- to say the least -- verbose (if you've done any work generating code with the .NET Framework's CodeDOM, this will bring back those nightmares). Plus, maintaining this code with all those hard-coded level numbers isn't going to be pleasant. Personally, I prefer the version with the Razor code.

By the way, there's nothing stopping you from combining these techniques. If my C# method was called BuildForm and returned only a single form, I could call that method from within Razor code like this:

@foreach (object obj in Model)
{
   if (obj is CustomerProfile)
   {
      @BuildForm("Profile")
   }
}

There are the tools available to you if you want to dynamically generate your page for its initial display. Just remember, once you've created the page, if you want to alter the HTML you'll need to use a different toolset.

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