Skip to main content

Model View Update (MVU) pattern using ASP .NET components and Blazor

A primer on Model View Update (MVU)

(UPDATE: The MVU library will be available via Blazique/Blazique (github.com) soon. The projects will be removed from Radix with the next major release after the move has been completed).

The Model-View-Update (MVU) architecture is a sophisticated blueprint for designing interactive programs, including web applications and games. This architecture is characterized by a unidirectional data flow and is composed of three integral components: the Model, the View, and the Update.

The Model is the heart of your application, encapsulating its state. The model preferably should be represented by an immutable data type.

The View function is tasked with creating the visual representation of your application. It is a pure function that takes the current model as input and returns a user interface description, such as HTML elements or graphics. The View function refrains from performing any side effects like mutating the application state, modifying the DOM, or sending requests; its sole purpose is to depict what should be displayed on the screen given the current state.

The Update is the only place that directly manipulates your model. It is a function that takes a message and the current model as input and returns a new model as output. A message is a command that gets created as a consequence of an event that triggers a change in your application, such as a user click, a timer tick, or a server response (like asynchronous SignalR messages). The update function should never mutate the model, but instead create an updated copy of it.



Benefits

Predictability

The unidirectional data flow makes it easy to reason about the state of your application.

Debuggability

The unidirectional data flow in MVU makes it easier to debug applications because you always know where your data comes from and where it goes.

Testability

MVU is highly testable because it relies on pure functions and immutable data. The pure functions (meaning that given a specific input, the function will consistently return the same output) in MVU make writing unit tests for the Model, View, and Update functions easy without mocking or stubbing anything. The model is a basic data structure that is trivial to test. Furthermore, the View and Update functions are both pure, resulting in deterministic output (The output of a pure function depends only on its input parameters and not on any external state or hidden information).

Since we don't rely upon markup for creating our output, but use pure functions, testing the output can be as simple as doing a comparison. Markup-based user interfaces, like Razor pages, are notoriously hard to test. Since functions are used to generate the output nodes (like elements and attributes), writing a test can be as easy as string comparisons. The same goes for the update function that is responsible for updating the model.

Downsides

Learning curve

MVU may have a steeper learning curve compared to other patterns, especially if you are new to functional programming or unidirectional data flow architectures. Understanding the concepts of immutability, pure functions, and unidirectional data flow can take some time.

Tooling and ecosystem

The tooling and ecosystem around MVU might not be as mature or extensive as other patterns like Model-View-Controller (MVC) or Model-View-ViewModel (MVVM). This means fewer libraries, frameworks, or resources are available specifically for MVU.

Building a library for C# / Blazor

As mentioned earlier, there are not many (any?) libraries that make building systems using MVU in C#, more specifically for Blazor, a breeze. Let's change that. We will draw inspiration from existing implementations in ELM and F# (Bolero) and make good use of some new language features that are coming up in C# 12.

Target audience

One question that may arise, is for whom this library would be the best fit. My answer to that would be teams that have a lot of C# expertise and prefer to stay within that context. Blazor is already very much suited to building applications in which both client-side (minimizing the need for JavaScript) as well as server-side code can be written in C#. This will take you the last mile so that even the markup code is pure C#.

Requirements

Reuse and compatibility

Blazor applications are created by composing components. By default components in Blazor are created using a combination of Razor pages and C#. We want our library to please nicely with all the bells and whistles that Blazor provides (like binding support and render modes), however, since we want to build components using MVU, we don't want to use a markup language like Razor. We do however want the MVU components to compose with Razor-based components. It should work both ways. We want to embed Razor components in our MVU components and vice versa.

Code layout

One of the advantages of a markup language is that the structure/layout of the view (nesting etc.) is easy to read. We want something similar, but we have to limit ourselves to the options C# (12) offers us.

Optimized for tree diffing algorithm

Blazor uses an algorithm for calculating the needed DOM that uses sequence numbers, which is very efficient (no tree traversals needed). For maximum efficiency, these sequence numbers should be compile-time constants, so that it is guaranteed that each time the components are rendered they have the exact same sequence number. This is very difficult to do when using dynamically generated sequence numbers at runtime. But we don't want to manually enter a sequence number into our code every time we add a component because this is tedious and error-prone. This is something we need to take into account when building our library.

ASP .NET Core components

Blazor is built on a component model in which a component is a self-contained user interface and processing logic that enables dynamic behavior. While IComponent is the root abstraction in this model, ComponentBase will be the base class we will use to build our foundation, since it contains the basic lifecycle events we will make use of.

DOM Abstraction

Updating the DOM directly in the browser using JavaScript is computationally intensive, so we want to minimize the number of updates we make to the DOM. We can do that by consolidating all logically related required DOM updates into 1 DOM operation. This is done by manipulating a lightweight copy of the DOM that is optimized for efficient updates (sometimes referred to as a shadow DOM). In Blazor this is the RenderTree. There are 2 instances of the RenderTree. Every time we do work a new copy is created. This new copy is compared to the previous version using a diffing algorithm. Then only the differences are applied to the actual DOM in the browser. In Blazor server applications these deltas are pushed using SignalR, when using WebAssembly, JavaScript interop is used to interact with the DOM.

To assist in the diffing algorithm, Blazor uses sequence numbers for each item in the RenderTree to help the algorithm with a direct indicator of what has changed (order, absence, or presence of objects for instance).

Razor components

Blazor has a strong bias towards building components based on Razor syntax. See this example of the Counter component that is included in some Blazor Visual Studio templates.

 

Razor files are ultimately compiled to C# resulting in pure code-based components, inheriting from ComponentBase. A benefit of this process is that the line numbers on which components are located can be used for generating sequence numbers that are used in the render tree diffing algorithm that Blazor uses. The resulting C# for the counter component looks like this.

 

Pure code components

Instead of writing Razor components, it is also possible to write components in pure code, manipulating the RenderTree using a RenderTreeBuilder (e.g. what is shown as the result of the Razor compilation above). This is the approach we will use for our library. The documentation makes us aware of a few things we need to keep an eye on when doing this. The most important one is that sequence numbers cannot be generated dynamically without a performance impact. Since each UI element must have a constant sequence number, generating a sequence number will fail when for instance some elements for instance are only present given a certain condition. So we need to let the user of the library decide on the sequence number to use, by providing a constant value, and/or a mechanism that can guarantee providing compile-time constant values automatically.

Design

Building components based on our library will be done by inheriting from 1 of 3 base classes.

Component

Component serves as the base for components that do not have a model, or interaction based on MVU. It is added for completeness, to enable users to write components using pure C# that only consist of markup. For this, it exposes one abstract member called Render.
It returns an array of instances of the delegate called Node that has the following signature:
Node instances are functions that provide access to the RenderTreeBuilder so that content can be added to the render tree. Each of these functions can express how a UI element or another component should be added to the RenderTree using the RenderTreeBuilder. For example, a naive implementation of a Node representing an HTML anchor ('a') tag could be: It adds an element with the name 'a' with sequence number 1 to the RenderTreeBuilder. Using this we can write an implementation of the Render method that returns a single anchor tag:
You might notice that it uses a syntax that may be unfamiliar. This is because we are using a new C# 12 feature called "Collection Expressions". This is a new terse syntax that can be used to create many different types, in this case, an array. Using this syntax we are a step closer to the readability of markup languages. This is what the implementation of the Component class looks like for now: Component implements the BuildRenderTree method that is called by the Blazor framework when the state of the component changes. In our case, it will call our Render method which will return an array of Node instances and then call all of them, while providing a reference to the current component instance and the RenderTreeBuilder. In our previous example, we only returned a single Node instance, so it will render a singular anchor tag. The example below is a direct port of the Razor (page) component that is in one of the Blazor project templates, named Index. This component has a Render method where a little more is happening. First of all, we see a 'component' function, we see an 'h1' function, and some 'text' functions. The 'component' function is a helper function that is used to render a component. It takes a type parameter that specifies the type of the component to render. The 'h1' function is a helper function that is used to render an HTML element, specifically an 'h1' element. The library provides helper functions for all HTML elements, events, attributes, and bindings (which will be shown later). You might have noticed that providing attributes for elements, like 'string' in the example, is optional. Children is a required parameter, though can be an empty array. Finally, the 'text' function is used to render a text node. There are many more helper functions like this, for instance for rendering attributes, events, etc. For attributes and events specialized delegates are used: The elements and components are Node instances. Attribute are specific delegates to add, you guessed it, attributes to the render tree. It needs to be specialized since we want to make sure we can only add attributes where attributes are expected. Attributes are not nodes and cannot exist outside of an element or component. Events are specialized attributes. Event is a specialized delegate that returns an Attribute instance for adding event handlers to the render tree.

The Index example above shows two instances of a Component delegate: ' component'. It is of course used to render components. In this case the PageTitle and the SurveyPrompt. The parameters are a list of attributes and a list of child nodes. If you look closely at the instance of the SurveyPrompt, you see that the Parameter "Title" is set through the 'attribute' function. This shows that we achieved our goal of being able to embed other (Razor) components in our components, including setting Parameters.

Component<T>

This version serves as an extension to the Component class, adding a model of type T that represents the state of the component. It also adds a method called ShouldRender that checks whether the model has any changes. Be sure to implement the equality members of T or use a record type when value equality is needed (which in 9 out of 10 cases should be the case). Let's look at an example of a component that uses a model. In this case, it is also a direct port of the Razor component that is in one of the Blazor project templates, named FetchData. It is a component that shows the weather forecast for a given location. It has a model that represents the state of the component, that contains a list of forecasts. The WeatherPage references the component and passes the initial state of the Model as a parameter. Notice that the component is rendered using server-side rendering. .NET 8 requires setting the render mode explicitly if the page needs interactivity. By default, all components are rendered using static rendering, which does not have support for interaction. Keep that in mind when you're scratching your head when your mouse clicks aren't having any effect! Aside from server-side rendering and static rendering, there are two more render modes, namely client-side rendering (WebAssembly) and auto, which is a hybrid of server-side and client-side rendering. In the latter case, the initial rendering is done on the server. In the meantime, the client-side code (dlls) is downloaded and initialized. On subsequent calls, the server-side rendered component is replaced by the client-side rendered component. This page also showcases the new StreamRendering functionality coming in .NET 8. 

Finally, I want to draw some attention to the layout of the markup in code. If we compare this with the Razor page equivalent, we can see we achieved our goal that we can create a similar layout which is quite good.



Component<TModel, TCommand>

Here is where the bread and butter for the implementation of the MVU pattern is baked in. It adds a method called Update that takes the current model and a command as input and returns a new model. Since this might contain asynchronous code but chances are it will often return synchronously, it returns a ValueTask instead of a regular task. Its task is to interpret the command and update the model accordingly. It also adds a method called View that takes a model and a delegate named 'dispatch' as input and returns an array of Node instances. The dispatch delegate is used to send commands to the Update method. This should be called in response to events that are triggered by the user or messages coming in in the background, for instance from a server or a SignalR connection. The implementation, the Dispatch method, calls the Update method and signals the fact the state (the model) has changed. The View method is called by the Blazor framework when the state of the component changes as part of the implementation of the Render method (ultimately called in the BuildRenderTree method). The Render method caches the old state (the model) as the PreviousModel and then calls the View method with the new state (Model) and the Dispatch method. The Component<TModel, TCommand> class looks like this: let's look at an example of a component that uses a model and implements the MVU pattern. A direct port of the Razor component as well, part of one of the Blazor project templates, named Counter. It contains a slight twist compared to the original since it only has a decrement button. The reason for this is that it showcases the use of the dispatch delegate with multiple types of commands. The Counter component has a model that contains a Count. It has 2 commands, Increment and Decrement, which are both implemented as implementations of CounterCommand, emulating a poor man's sum type. The point of attention here is that in the View method, we handle the 'on click' event of the button and call the dispatch delegate with a new instance of a CounterCommand command. The Update will pattern-match on the command and update the model accordingly. This triggers a re-render of the component because the state has changed and the state (Model) is different from the PreviousModel. The razor page including the Counter component looks like this:

In the final example, a component is shown that adds a new item to an inventory, showing the data binding capabilities of the library. Pay special attention to lines 77, 96, and 114. These lines show how the value of the input element is bound to a property of the model. This is where we deviate for a moment from the original pure MVU pattern. The 'pure' way of handling updates to the model is by sending a command to the update function. In this case, you would respond to the 'onchange' event of the input element and send a command to the update function using the Dispatch function argument. So you would have to define a command for every property that needs to be updated or 1 command with all properties.

Here I opt to update the model directly through the data binding mechanism. This is not pure, but might be practical. The update function is still pure, but the updates to the model are not. This is a trade-off. Both have their pros and cons. The pure way is more explicit, consistent with the rest of the code, and might be easier to reason about. The data-binding way is more concise and might be easier to use. Anyway, support is there for both, you can make your own choice.

Try it out for yourself

If you want to try using the library for yourself, add a reference to the NuGet package Radix.Interaction.Web : NuGet Gallery | Radix.Interaction.Web 4.4.0 . The source code for the library can be found at my GitHub repo : https://github.com/MCGPPeters/Radix  . Look for projects starting with Radix.Interaction . Radix.Interaction.Web.Demo is a little demo project using the concepts presented above.

Comments

Popular posts from this blog

Running Microsoft Playwright in an Azure Function using C#

When you have tried to run MS Playwright using C# in the context of an Azure Function, you probably have run into this message: The driver it is referring to resides in the .playwright folder that is copied to the build output folder. Now, the output folder structure of an Azure Function project is different from most projects in the sense that there is an extra nested bin folder where the drivers should actually be copied.  The build target that the Playwright team uses, at the time of writing (version 1.15.4), always copies the folder containing the driver to the root of the output folder. So the fix here is to add an extra build target to your project file, the corrects for the extra nested folder:   <Target Name="FixPlaywrightCopyAfterBuild" AfterTargets="Build">     <ItemGroup>       <_BuildCopyItems Include="$(OutDir).playwright\**" />     </ItemGroup>     <Message Text="[Fix] Copying files to the nested bin folder o

Type aliases using C# Source Generators and C# 10 generic attributes

When practicing domain-driven design, a reoccurring chore is the creation of value types. These are strongly typed representations of simple types like strings that have a specific meaning, like CustomerId or ProductCode. These could both be strings but we put preferably implement them as strongly typed variations so that we can't for instance mix up multiple string parameters while coding. In some languages, this is something that comes out of the box and is often referred to as type aliasing. C# does not support this. Although in C# you could give another name to a string type with a using statement, it still remains a string (the type does not change).  This task is so common and tedious that it makes it the perfect case for implementing a source generator. Creating a type alias should be as simple as adding an attribute indicating what type should be aliased. It should also work for all kinds of types, like class, record, struct, and record struct. The full source code can be f