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.
Comments
Post a Comment