Note: the code for this example uses Blazor WebAssembly 3.2.0 RTM
This demo application shows how Blazor's component model permits us to easily encapsulate code, UI and behaviours in reusable modules, and even load components dynamically in code.
A demo is hosted at https://blazordynamiclist.azurewebsites.net
I currently have a large ASP.NET web application which has services for a wide range of products and services it must support.
Each product has its own settings, properties and behavior. Although I can encapsulate the product behaviours in class libraries, it's been very hard to create UI in these class libraries as neither ASP.NET or JavaScript really lends themselves to this.
This results in a lot of supplier-specific and product-specific UI in the ASP.NET project, resulting in a monolithic web app that is only losely bound to the product code.
When Blazor came onto the scene in 2017, I was excited for two reasons.
First, the ability to use C# in the client meant we no longer had to re-write the same C# code in JavaScript to get front-end behaviours. The server and the client can share the models.
Secondly, and in my opinion more importantly, Blazor's excellent component model permits us to encapsulate the UI in these libraries as well.
So I decided to write this project as a proof-of-concept. Could I encapsulate behaviours and UI in libraries, and then handle generic lists of objects and display the correct UI for each one?
The business case is a product list, where each product has some common
features, e.g. Name
and Price
but also specific properties for each type.
Product1 has a HasFlange
property and Product2 has a Grommets
property. We want to have a custom view that shows each product according to
its type and is contained in the same library that defines the ProductX class.
Component1
displays a Product1 and Component2
displays a Product2.
In a further complication I decided that any Product1 that has HasFlange
set as true should use the Component1b
component.
The project uses the Blazor (ASP.NET Core hosted) template as a starting point.
I created a .NET Standard library BaseClasses to hold a common ProductBase
abstract base class, which has ID
, Name
, Price
and Image
properties.
It also defines an abstract method GetViewComponent()
that returns the
type of the Razor Component we want to use to view the product.
I then added new Blazor library projects Library1 and Library2 using
the console command dotnet new blazorlib -o Library1
etc. These represent
our two product libraries. Product1
and Product2
inherit from ProductBase
.
I also created a RepositoryLib .NET Standard library which represents
our 'datasource' (which in a real-world example would be a database of
some kind). The ProductRepository
class just generates random products
of either type using the GetProducts()
method.
The Index.razor
page on the client I used to test the basic component
binding. Component1
and Component2
are placed in the HTML with binding
to page properties p1
and p2
respectively.
These are initially null
but are populated using the button. You should
see the products render when the button is clicked.
I modified the FetchData.razor
page to use a list of ProductBase
. The
initalization code calls the WebAPI method on the server to fetch a random
list of products.
Although the FetchData list is ProductBase
, we cannot use this code:
products = await Http.GetJsonAsync<ProductBase>("api/SampleData/Products?count=6");
The deserializer cannot create instances of the abstract class ProductBase
, and
anyway we want the specific Product1
and Product2
instances. The solution
is to use the NewtonsoftTypeNameHandling
option on the server API to generate JSON with embedded types, and then
deserialize this on the client in the same way. I created a simple class
TypedSerializer
in my BaseClasses library to achieve this.
The FetchData page can then deserialize as follows:
protected override async Task OnInitAsync()
{
// get sample data as JSON string
var json = await Http.GetStringAsync("api/SampleData/Products?count=6");
// deserialize the list using a typed-deserializer
var data = BaseClasses.TypedSerializer.Deserialize(json);
// set value
products = (ProductBase[])data;
}
The page shows the list of products in three different ways.
The first just shows a list of ProductBase
values - only the common
properties can be used when we do this.
The second list shows a manual if-then-else
type of binding where we check
the product type and show a bound component manually selecting either
<Component1>
or <Component2>
.
@foreach (var product in products)
{
@* here we manually bind - simple with two, but quickly becomes untenable with say a hundred product types! *@
@if (product is Library1.Product1 p1)
{
<Library1.Component1 Product=@p1></Library1.Component1>
}
@if (product is Library2.Product2 p2)
{
<Library2.Component2 Product=@p2></Library2.Component2>
}
}
While this works for two products, if we had hundreds or even thousands of products this becomes a nightmare. Worse, it's putting product specific logic into the web application and making maintaining it much harder.
The third list is the cool one! We display each product using a DynamicComponent
:
@foreach (var product in products)
{
<DynamicComponent Product=@product />
}
This dynamic component selects and binds the correct component for each product.
Also notice it uses the correct <Component1b>
if the HasFlange
property is set.
The code for this class is in the root of the Blazor client (although really it
should be in the BaseClasses library). This is a manually coded Razor Component
that determines which component to use by calling the GetViewComponent()
method.
It then manually builds the render tree, binding the property thus:
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
Type componentType = Product.GetViewComponent();
builder.OpenComponent(0, componentType);
builder.AddAttribute(1, "Product", Product);
builder.CloseComponent();
}
I figured out the syntax by simply looking at the generated C# code for the Index.razor
page in the file BlazorDynamicList.Client\obj\Debug\netstandard2.0\Razor\Pages\Index.razor.g.s
You may notice both product libraries come with styles.css
and
background.png
files. These are loaded by Blazor for us as virtual
libraries e.g. /_content/Library1/styles.css
Thanks to all those fellow Blazorians on Gitter and especially to Chris Sainty for his excellent Blog articles on how to use Github and Azure pipelines to publish the site.
Updated application to .NET Core 3.1 and .NET Standard 2.1, and now to Blazor 3.2 RTM as Blazor WASM is now an official, supported product!