Mastering Blazor - Part 1: DOM Integration
Tags: blazor, dotnet, javascript, webdev
Published: 2022-03-31 12:12:27

*Photo by <a href="https://unsplash.com/@kaleidico?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kaleidico</a> on <a href="https://unsplash.com/s/photos/integration?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>*
  

In the recent two years I did quite a bit of web development also using Microsoft's new web framework called Blazor. Blazor adds component-first support to ASP.NET by introducing "Razor components". With Razor components Blazor is capable of providing a full single-page application framework.

## Bootstrapping

The way Blazor works is by running your .NET code either on the server-side with a bridge written in WebSocket or on the client-side using WebAssembly. In both cases Blazor integrates into your page via a couple of globals. Most importantly `Blazor`. For instance, using `window.Blazor.start()` you can start Blazor manually if you prevented the automatic start. This works via

```html
<script src="blazor.webassembly.js" autostart="false"></script>
```

The location where this file is received from is quite crucial as it will take and load another file from it: *blazor.boot.json*.

The Blazor boot metadata file contains important information such as `cacheBootResources` (should the resources be cached or not?), the entry assembly, or all the resources to load. The latter is an object using the `resources` property with sub properties `assembly`, `lazyAssembly`, `pdb`, `runtime`, and `satelliteResources`.

Using this information Blazor will begin downloading everything. Crucial here is the runtime, notably `dotnet.5.0.5.js` and `dotnet.wasm`. The former brings another global: `DotNet`. The latter brings the CLR, which is responsible for actually running the .NET code. Actually, the `.dll` files are technically not different than when you'd run on the server. It's just that instead of having the CLR somewhat integrated into the OS we integrated it into the browser. It's now run via WebAssembly.

This is not the only available mode. There is no requirement on including those files. Alternatively, one may actually choose the WebSocket renderer. In this variant, instead of talking to WASM sitting in the same browser, the runtime of Blazor will communicate with some server via WebSocket. Now instead of receiving frames and doing interop via JS <-> WASM it is done via JS <-> WebSocket <-> Server.

As mentioned, Blazor assumes that the manifest and its dependencies are all coming from the same directory as the loading page. Right now, there is no easy way to change it. What we can do, however, is to rewire that. The following snippet creates a function to start up Blazor, i.e., to not just call `Blazor.start()` but instead call `starter()` where `starter` was created from calling the following function:

```js
function createBlazorStarter(publicPath) {
  if (publicPath) {
    const baseElement =
      document.head.querySelector('base') || document.head.appendChild(document.createElement('base'));
    const originalBase = baseElement.href;
    baseElement.href = publicPath;
    return () => {
      window.Blazor._internal.navigationManager.getBaseURI = () => originalBase;
      return window.Blazor.start().then(() => {
        baseElement.href = originalBase;
      });
    };
  }

  return () => window.Blazor.start();
}
```

Already from this little snippet you can see that there is a bit more to it than meets the eye. You can spot the `_internal` property on `window.Blazor`, which hosts a couple of necessary interop services. One of these is `navigationManager`, which is the JS counterpart to the Blazor router.

In the previous snippet we modify the `getBaseURI` function to return the base URL, which will be used to construct general calls. Besides this, there are a couple other functions on `_internal`, too:

- `attachRootComponentToElement`
- `getApplicationEnvironment`
- `getConfig`
- `getSatelliteAssemblies`
- `renderBatch`

These functions are not so much of practical use. They are used by Blazor to trigger behavior, e.g., render outstanding frames or get the initial application environment. The calls are always executed via `DotNet`. In C#/.NET this would look like:

```js
Microsoft.JSInterop.JSRuntime.InvokeVoidAsync("Blazor._internal.renderBatch", arg1, arg2, ...);
```

This will use special functionality that comes with the respective .NET bridge. In WebAssembly this will make a call via the WASM runtime. On the other hand, we can also call .NET code from JavaScript via `DotNet`. Example:

```js
window.DotNet.invokeMethodAsync('MyLib', 'DotNetFunctionName', arg1, arg2, ...);
```

While JavaScript functions need to be globally available (i.e., attached to `window`) the basic requirement for .NET functions to be called is a special attribute: `JSInvokable`. There's a little bit more to it (like instance-bound vs static), but in general that's covered pretty well by the [official documentation about it](https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0).

With that being written let's move on to look at what events are handled / forwarded specifically in Blazor.

## Events

The following events are treated specifically by Blazor and need to be dispatched to it:

- `abort`
- `blur`
- `change`
- `error`
- `focus`
- `load`
- `loadend`
- `loadstart`
- `mouseenter`
- `mouseleave`
- `progress`
- `reset`
- `scroll`
- `submit`
- `unload`
- `DOMNodeInsertedIntoDocument`
- `DOMNodeRemovedFromDocument`
- `click`
- `dblclick`
- `mousedown`
- `mousemove`
- `mouseup`

Blazor listens to these events on the "root node", i.e., the element where you bootstrap the application to. This is usually a custom element called `<app>`, but you can actually change this in the Blazor startup.

The following snippet sets the node to be an element with the ID `blazor-root`:

```cs
public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    var baseAddress = new Uri(builder.HostEnvironment.BaseAddress);

    builder.RootComponents
        .Add<App>("#blazor-root");

    builder.Services
        .AddSingleton(new HttpClient { BaseAddress = baseAddress });

    await builder.Build().RunAsync();
}
```

Usually, you'll not need to know these events and how Blazor demands and listens to them. However, if you want to do some special stuff - such as DOM projections, where you take out a node below the root node and attach it somewhere else in the DOM then you need to use the list above for forwarding such events.

Event forwarding is in general not difficult, however, it might come with some edge cases. For instance, on some browsers the `MutationEvent` coming from events like `DOMNodeInsertedIntoDocument` cannot be cloned and therefore not be dispatched again (easily).

In general an event clone / re-dispatch looks as follows:

```js
function reDispatchEvent(newTarget, originalEvent) {
  const eventClone = new originalEvent.constructor(originalEvent.type, originalEvent);
  newTarget.dispatchEvent(eventClone);
}
```

With that in mind let's also go briefly over the topic of serialization as it gets important for any kind of interop.

## Serialization

As mentioned initially Blazor does not really live in JavaScript. Instead, only a "tiny" management layer lives in JavaScript - exposed via global `Blazor` and `DotNet` variables. Instead, Blazor either renders on the server or inside a WASM-powered CLR runtime. Theoretically, we could also introduce another way of including Blazor - the possibility is there.

In any case, the chosen approach means that we need to have a message exchange between the system running Blazor (e.g., inside WebAssembly) and the page. The message exchange is based on a string, so anything send there must be serializable. The easiest format is a JSON-based serialization, which allows us to use plain JS objects as message input.

The downfall of this approach is that there are certain things that seem possible at first, but cannot be serialized. Take for instance the following snippet:

```js
JSON.stringify({
  a: true,
  b: 'foo',
  c: () => console.log('Hello'),
});
```

It may seem possible at first, however, since JSON is platform and language independent there would be no notion of a function. Therefore, functions are just discarded resulting in:

```js
{"a":true,"b":"foo"}
```

So far, so good. Nevertheless, even worse than discarding certain elements is that the serialization can also just fail and error out. Consider this:

```js
const obj = {};
obj.parent = obj;
JSON.stringify(obj);
```

Running this will result in an error being thrown: **Uncaught TypeError: cyclic object value**. Apparently, since the JSON format is just a string there is no capability of including references there. Just serializing the object again would yield an infinite long strong (due to the endless recursion). One way of dealing with that is to perform some sanatization when serializing:

```js
const obj = {};
obj.parent = obj;
JSON.stringify(obj, (key, value) => {
  if (key == 'parent') {
    return '$self';
  } else {
    return value;
  }
});
```

Now this results in the following JSON:

```js
{"parent":"$self"}
```

The `$self` we could now use as a special notation when deserializing. Alternatively, we could have also discarded it by returning `undefined`.

Alright, but there is a bit more to serialization than just understanding of JSON. For many things, e.g., network calls, using JSON as serialization format would not be good. In fact, using a string as a message would not be good. Instead, we need to understand that the native way of communicating with WebAssembly is a chunk of bytes - an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).

In order to work with all these serialization types (and more) the `Blazor.platform` utilities may be helpful. The implementation of these are really exclusive to the WebAssembly platform (called `MonoPlatform`, see, e.g., [an older GitHub snapshot](https://github.com/dotnet/aspnetcore/blob/e7d5306202a51949ec9e5c3f020c13fd4837099e/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts) for more details).

We find:

- `start`: Begins bootstrapping WebAssembly
- `callEntryPoint`: Actually does the CLR bootstrapping against `Microsoft.AspNetCore.Components.WebAssembly` using `Microsoft.AspNetCore.Components.WebAssembly.Hosting.EntrypointInvoker`
- `getArrayEntryPtr`: Find the address of a field within a .NET array
- `getArrayLength`: Gets the length of a .NET array
- `getObjectFieldsBaseAddress`: Same as with arrays, just for a .NET object
- `readFloatField`: Gets the single value from an object and an offset
- `readInt16Field`: Gets the short value from an object and an offset
- `readInt32Field`: Gets the int value from an object and an offset
- `readObjectField`: Gets an arbitrary class instance from an object and an offset
- `readStringField`: Gets the string value from an object and an offset
- `readStructField`: Gets an arbitrary struct value from an object and an offset
- `readUint64Field`: Gets the long value from an object and an offset
- `toUint8Array`: Converts a .NET array to an `Uint8Array`

Long story short, these functions are used under the hood to actually convert .NET's data types to some JavaScript. Note that all these different number types are still just `number` in JavaScript, but need to be distinguished as they use different amounts of bytes and / or representations. For instance, both, a single floating point number (`float`) and a standard integer (`int`) are both 32 bytes, but one is using [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) while the other has no IEEE standard and follows standard weighted ordering.

A great use-case of these platform functions is to help dealing with larger files. As described by [Gérald Barré](https://www.meziantou.net/generating-and-downloading-a-file-in-a-blazor-webassembly-application.htm) the actual message cost can be severely reduced using things such as `BINDING.conv_string` and `Blazor.platform.toUint8Array`.

Using this instead you'd get quite a speed up (in his sample the orange line represents the approach where we'd need to use the `Blazor.platform` function):

![Speedup by better serialization](https://www.meziantou.net/assets/performancechart.png?v=6e99)

Doing less work can mean choosing fewer (and right) serialization schemes.

## Conclusion

In this article we started with a closer look at how Blazor works internally by inspecting how Blazor is actually coupled to the DOM and what implications arise from its integration.

In the next article I'll write about how Blazor's virtual DOM actually works and how it renders things out.