- Introduction
- Motivation
- Features
- Installation
- Usage
- Running the Tests
- Contributing
- License
- Contact
- Acknowledgements
- Contributors
BlazorMVU is a library that implements the Model-View-Update (MVU) pattern for Blazor. It provides a structured way to organize your Blazor components and manage their state, making your code more understandable and easier to maintain.
Demo: This Blazor project is deployed on GitHub Pages
The Elm architecture, or Model-View-Update (MVU), is a simple yet powerful pattern for structuring applications. It has gained popularity due to its simplicity, maintainability, and robustness. However, despite its advantages, the Elm architecture has not been widely adopted in the Blazor community.
Blazor, as a framework, is flexible and allows for various design patterns to be implemented, including MVU. However, there hasn't been a straightforward way to implement the Elm architecture in Blazor - until now.
The motivation behind BlazorMvu is to bring the benefits of the Elm architecture to the Blazor community. By providing a library that implements the MVU pattern, we aim to make it easier for developers to structure their Blazor applications in a way that is easy to understand, maintain, and test.
We believe that the Elm architecture can greatly improve the developer experience when building Blazor applications. By reducing the complexity associated with state management and UI updates, developers can focus more on the business logic of their applications, leading to more robust and reliable software.
We hope that BlazorMvu will serve as a valuable tool for the Blazor community and contribute to the growth and maturity of Blazor as a framework for building web applications.
- MVU Pattern Implementation - Full Model-View-Update architecture for Blazor
- SimpleMvuComponent - Lightweight base class for simple components
- MvuComponent - Full-featured base class with advanced capabilities
- Commands (Cmd) - Declarative side effects handling
Cmd.OfTask- Async operations that return messagesCmd.OfMsg- Immediate message dispatchCmd.Batch- Combine multiple commandsCmd.Delay- Delayed message dispatch
- Subscriptions (Sub) - External event listeners
Sub.Timer- Interval-based updatesSub.Timeout- One-time delayed messagesSub.Custom- Custom subscription logic
- Middleware - Dispatch pipeline interceptors
- Logging middleware
- Timing middleware
- Debounce/Throttle middleware
- Error handling middleware
- Time-Travel Debugging - Navigate through state history
- State Persistence - localStorage/sessionStorage integration
- MvuResult - Functional result type for error handling
- Counter, Text Reverser, Password Form, Todo List
- Fetch with error handling
- Stopwatch with subscriptions
- Shopping Cart with commands and async
- Parent-Child communication patterns
- Unit tests using BUnit and xUnit v3
- Shouldly assertions
Clone the repository and build the project:
git clone https://github.com/Atypical-Consulting/BlazorMvu.git
cd BlazorMvu
dotnet buildFor simple components, inherit from SimpleMvuComponent<TModel, TMsg>:
@inherits BlazorMVU.SimpleMvuComponent<int, MvuCounter.Msg>
<div class="grid">
<button @onclick="@(() => Dispatch(new Msg.Decrement()))">-</button>
<input type="text" value="@State" disabled />
<button @onclick="@(() => Dispatch(new Msg.Increment()))">+</button>
</div>
@code {
// Messages using discriminated unions
public abstract record Msg
{
public record Increment : Msg;
public record Decrement : Msg;
}
protected override int Init() => 0;
protected override int Update(Msg msg, int model)
=> msg switch
{
Msg.Increment => model + 1,
Msg.Decrement => model - 1,
_ => model
};
}For components that need side effects, inherit from MvuComponent<TModel, TMsg>:
@inherits BlazorMVU.MvuComponent<FetchExample.Model, FetchExample.Msg>
@code {
public record Model(string? Data, bool IsLoading, string? Error);
public abstract record Msg
{
public record FetchData : Msg;
public record DataReceived(MvuResult<string> Result) : Msg;
}
// Return both model and command
protected override (Model, Cmd<Msg>) InitWithCmd()
=> (new Model(null, true, null), Cmd.OfMsg<Msg>(new Msg.FetchData()));
protected override (Model, Cmd<Msg>) UpdateWithCmd(Msg msg, Model model)
=> msg switch
{
Msg.FetchData => (
model with { IsLoading = true },
Cmd.OfTask<Msg>(async ct => {
var result = await FetchDataAsync(ct);
return new Msg.DataReceived(result);
})),
Msg.DataReceived received => (
received.Result.IsSuccess
? model with { Data = received.Result.Value, IsLoading = false }
: model with { Error = received.Result.Error?.Message, IsLoading = false },
Cmd.None<Msg>()),
_ => (model, Cmd.None<Msg>())
};
}Override the Subscriptions method to react to external events:
protected override Sub<Msg> Subscriptions(Model model)
{
if (model.IsRunning)
{
// Tick every 100ms while running
return Sub.Timer<Msg>(
TimeSpan.FromMilliseconds(100),
now => new Msg.Tick(now),
"timer-id");
}
return Sub.None<Msg>();
}Add middleware in OnInitialized:
protected override void OnInitialized()
{
UseMiddleware(
Middleware.ConsoleLogger<Model, Msg>(),
Middleware.Timing<Model, Msg>((msg, elapsed) =>
Console.WriteLine($"{msg} took {elapsed.TotalMilliseconds}ms"))
);
base.OnInitialized();
}Enable time-travel debugging via parameter:
<MyComponent EnableTimeTravel="true" TimeTravelMaxHistory="50" />Then access the debugger:
// Go back in history
Debugger?.GoBack();
RestoreFromDebugger();
// Go forward
Debugger?.GoForward();
RestoreFromDebugger();Tests are located in the BlazorMvu.Tests project. You can run them using the .NET Core CLI:
dotnet testContributions are welcome! Please read the CONTRIBUTION GUIDELINES first.
This project is licensed under the terms of the MIT license. If you use this library in your project, please consider adding a link to this repository in your project's README.
This project is maintained by Atypical Consulting. If you need help with this project, please contact us from this repository by opening an issue.
You can contact us by opening an issue on this repository.
