Skip to content

πŸ”₯ Blazing.Mvvm - Full MVVM support for Blazor with CommunityToolkit.Mvvm integration. Supports all hosting models (Server, WASM, SSR, Auto, Hybrid, MAUI). Features strongly-typed navigation, automatic ViewModel registration, parameter resolution, validation support, and comprehensive lifecycle management. Includes samples and full documentation.

License

Notifications You must be signed in to change notification settings

gragra33/Blazing.Mvvm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

219 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Blazor Extension for the MVVM CommunityToolkit

NuGet Version NuGet Downloads .NET 8+

πŸ”₯ Blazing.Mvvm brings full MVVM support to Blazor applications through seamless integration with the CommunityToolkit.Mvvm. This library supports all Blazor hosting models, including Server, WebAssembly (WASM), Static Server-Side Rendering (SSR), Auto, Hybrid (WPF, WinForms, Avalonia), and MAUI. It features strongly-typed ViewModel-first navigation, automatic ViewModel registration and discovery, parameter resolution between Views and ViewModels, validation support with ObservableValidator, and comprehensive lifecycle management. The library includes extensive sample projects and complete documentation to help you get started quickly.

Table of Contents

Quick Start

Installation

Add the Blazing.Mvvm NuGet package to your project.

Install the package via .NET CLI or the NuGet Package Manager.

.NET CLI

dotnet add package Blazing.Mvvm

NuGet Package Manager

Install-Package Blazing.Mvvm

Configuration

Configure the library in your Program.cs file. The AddMvvm method adds the required services for the library and automatically registers ViewModels that inherit from ViewModelBase, RecipientViewModelBase, or ValidatorViewModelBase in the calling assembly.

using Blazing.Mvvm;

builder.Services.AddMvvm(options =>
{ 
    options.HostingModelType = BlazorHostingModelType.WebApp;
});

Note

Since v3.1.0, the BasePath property is automatically detected from the application's base URI and is no longer required for subpath hosting or YARP scenarios. See the Subpath Hosting section for details.

If you are using a different hosting model, set the HostingModelType property to the appropriate value. The available options are:

  • BlazorHostingModelType.Hybrid
  • BlazorHostingModelType.Server
  • BlazorHostingModelType.WebApp
  • BlazorHostingModelType.WebAssembly
  • BlazorHostingModelType.HybridMaui

Registering ViewModels in a Different Assembly

If the ViewModels are in a different assembly, configure the library to scan that assembly for the ViewModels.

using Blazing.Mvvm;

builder.Services.AddMvvm(options =>
{ 
    options.RegisterViewModelsFromAssemblyContaining<MyViewModel>();
});

// OR

var vmAssembly = typeof(MyViewModel).Assembly;
builder.Services.AddMvvm(options =>
{ 
    options.RegisterViewModelsFromAssembly(vmAssembly);
});

Usage

Create a ViewModel inheriting the ViewModelBase class

[ViewModelDefinition(Lifetime = ServiceLifetime.Scoped)]
public sealed partial class FetchDataViewModel : ViewModelBase, IDisposable
{
    private readonly IWeatherService _weatherService;
    private readonly ILogger<FetchDataViewModel> _logger;
    private readonly CancellationTokenSource _cancellationTokenSource = new();

    [ObservableProperty]
    private IEnumerable<WeatherForecast>? _weatherForecasts;

    public string Title => "Weather forecast";

    public FetchDataViewModel(IWeatherService weatherService, ILogger<FetchDataViewModel> logger)
    {
        _weatherService = weatherService;
        _logger = logger;
    }

    public override async Task OnInitializedAsync()
    {
        WeatherForecasts = await _weatherService.GetForecastAsync() ?? [];
    }

    public void Dispose()
    {
        _logger.LogInformation("Disposing {VMName}.", nameof(FetchDataViewModel));
        _cancellationTokenSource.Cancel();
        _cancellationTokenSource.Dispose();
    }
}

Create your Page inheriting the MvvmComponentBase<TViewModel> component

Note

If working with repositories, database services, etc., that require a scope, then use MvvmOwningComponentBase<TViewModel> instead.

@page "/fetchdata"
@inherits MvvmOwningComponentBase<FetchDataViewModel>

<PageTitle>@ViewModel.Title</PageTitle>

<h1>@ViewModel.Title</h1>

@if (ViewModel.WeatherForecasts is null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in ViewModel.WeatherForecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

Give a ⭐

If you like this project or are using it to learn or start your own solution, please give it a star. Thanks!

Also, if you find this library useful, and you're feeling really generous, then please consider buying me a coffee β˜•.

Documentation

The Library supports the following hosting models:

  • Blazor Server App
  • Blazor WebAssembly App (WASM)
  • Blazor Web App (.NET 8.0+)
  • Blazor Hybrid - Wpf, WinForms, MAUI, and Avalonia (Windows only)

The library package includes:

  • MvvmComponentBase, MvvmOwningComponentBase (Scoped service support), & MvvmLayoutComponentBase for quick and easy wiring up ViewModels.
  • ViewModelBase, RecipientViewModelBase, & ValidatorViewModelBase wrappers for the CommunityToolkit.Mvvm.
  • MvvmNavigationManager class, MvvmNavLink, and MvvmKeyNavLink component for MVVM-style navigation, no more hard-coded paths.
  • Sample applications for getting started quickly with all hosting models.

View Model

The library offers several base classes that extend the CommunityToolkit.Mvvm base classes:

Lifecycle Methods

The ViewModelBase, RecipientViewModelBase, and ValidatorViewModelBase classes support the ComponentBase lifecycle methods, which are invoked when the corresponding ComponentBase method is called:

  • OnAfterRender
  • OnAfterRenderAsync
  • OnInitialized
  • OnInitializedAsync
  • OnParametersSet
  • OnParametersSetAsync
  • ShouldRender

IDisposable Implementation

Note

Added v3.2.1, all ViewModel base classes (ViewModelBase, RecipientViewModelBase, and ValidatorViewModelBase) now implement IDisposable to provide automatic cleanup of PropertyChanged event subscriptions for IAsyncRelayCommand instances.

Automatic Cleanup: When a ViewModel is disposed, it automatically unsubscribes from all IAsyncRelayCommand PropertyChanged events, preventing memory leaks and ensuring proper resource cleanup. This is particularly important for commands with AllowConcurrentExecutions set to false, where the framework monitors the command's IsRunning property to trigger UI updates.

Manual Disposal in Derived Classes: If you need to dispose of additional resources in your ViewModel, override the Dispose(bool disposing) method:

[ViewModelDefinition(Lifetime = ServiceLifetime.Scoped)]
public sealed partial class MyViewModel : ViewModelBase
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
    
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Dispose of your managed resources here
            _cancellationTokenSource.Cancel();
            _cancellationTokenSource.Dispose();
        }
        
        // Always call base to ensure command subscriptions are cleaned up
        base.Dispose(disposing);
    }
}

Warning

If your ViewModel previously implemented IDisposable manually, you must change public void Dispose() to protected override void Dispose(bool disposing) to avoid build errors. The base classes now handle the IDisposable pattern implementation.

Benefits:

  • βœ… Automatic Memory Leak Prevention - Command event subscriptions are automatically cleaned up
  • βœ… Simplified Code - No need to manually track and unsubscribe from command events
  • βœ… Consistent Pattern - All ViewModels follow the standard .NET dispose pattern
  • βœ… Better Performance - Proper cleanup ensures commands and ViewModels are garbage collected efficiently

Service Registration

ViewModels are registered as Transient services by default. If you need to register a ViewModel with a different service lifetime (Scoped, Singleton, Transient), use the ViewModelDefinition attribute:

[ViewModelDefinition(Lifetime = ServiceLifetime.Scoped)]
public partial class FetchDataViewModel : ViewModelBase
{
    // ViewModel code
}

In the View component, inherit the MvvmComponentBase type and set the generic argument to the ViewModel:

@page "/fetchdata"
@inherits MvvmComponentBase<FetchDataViewModel>
Registering ViewModels with Interfaces or Abstract Classes

To register the ViewModel with a specific interface or abstract class, use the ViewModelDefinition generic attribute:

[ViewModelDefinition<IFetchDataViewModel>]
public partial class FetchDataViewModel : ViewModelBase, IFetchDataViewModel
{
    // ViewModel code
}

In the View component, inherit the MvvmComponentBase type and set the generic argument to the interface or abstract class:

@page "/fetchdata"
@inherits MvvmComponentBase<IFetchDataViewModel>
Registering Keyed ViewModels

To register the ViewModel as a keyed service, use the ViewModelDefinition attribute (this also applies to a generic variant) and set the Key property:

[ViewModelDefinition(Key = "FetchDataViewModel")]
public partial class FetchDataViewModel : ViewModelBase
{
    // ViewModel code
}

In the View component, use the ViewModelKey attribute to specify the key of the ViewModel:

@page "/fetchdata"
@attribute [ViewModelKey("FetchDataViewModel")]
@inherits MvvmComponentBase<FetchDataViewModel>

Parameter Resolution

The library supports passing parameter values to the ViewModel from the View.

This feature is opt-in. To enable it, set the ParameterResolutionMode property to ViewAndViewModel in the AddMvvm method. This will resolve parameters in both the View component and the ViewModel.

builder.Services.AddMvvm(options =>
{ 
    options.ParameterResolutionMode = ParameterResolutionMode.ViewAndViewModel;
});

To resolve parameters in the ViewModel only, set the ParameterResolutionMode property value to ViewModel.

Properties in the ViewModel that should be set must be marked with the ViewParameter attribute.

public partial class SampleViewModel : ViewModelBase
{
    [ObservableProperty]
    [property: ViewParameter]
    private string _title = default!;

    [ObservableProperty]
    [property: ViewParameter("Count")]
    private int _counter;

    [ViewParameter]
    public string? Content { get; set; }
}

In the View component, the parameters should be defined as properties with the Parameter attribute:

@page "/sample"
@inherits MvvmComponentBase<SampleViewModel>

@code {
    [Parameter]
    public string Title { get; set; } = default!;

    [Parameter]
    public int Count { get; set; }

    [Parameter]
    public string? Content { get; set; }
}

Automatic Two-Way Binding

Added v3.2.0, Blazing.Mvvm automatically handles two-way binding between View components and ViewModels when using the @bind- syntax, eliminating the need for manual PropertyChanged event handling.

When a component has:

  • An EventCallback<T> parameter following Blazor's {PropertyName}Changed naming convention (e.g., CounterChanged)
  • A corresponding ViewModel property marked with [ViewParameter] (e.g., Counter)

The two-way binding is automatically wired up. When the ViewModel property changes, the EventCallback is invoked automatically with zero configuration and automatic memory leak prevention.

Before (Manual Event Handling):

ViewModel:

public partial class CounterComponentViewModel : ViewModelBase
{
    [ObservableProperty]
    [property: ViewParameter]
    private int _counter;
}

Component (Required 30+ lines of boilerplate):

@using System.ComponentModel
@inherits MvvmComponentBase<CounterComponentViewModel>

<p role="status">Current count: <strong>@ViewModel.Counter</strong></p>

@code {
    [Parameter]
    public int Counter { get; set; }

    [Parameter]
    public EventCallback<int> CounterChanged { get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ViewModel.PropertyChanged += OnViewModelPropertyChanged;
    }

    private async void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(ViewModel.Counter) && ViewModel.Counter != Counter)
        {
            await CounterChanged.InvokeAsync(ViewModel.Counter);
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            ViewModel.PropertyChanged -= OnViewModelPropertyChanged;
        }
        base.Dispose(disposing);
    }
}

After (Automatic Two-Way Binding):

ViewModel (unchanged):

public partial class CounterComponentViewModel : ViewModelBase
{
    [ObservableProperty]
    [property: ViewParameter]
    private int _counter;
}

Component (Just 9 lines!):

@inherits MvvmComponentBase<CounterComponentViewModel>

<p role="status">Current count: <strong>@ViewModel.Counter</strong></p>

@code {
    [Parameter]
    public int Counter { get; set; }

    [Parameter]
    public EventCallback<int> CounterChanged { get; set; }
}

Usage in Parent Component:

<CounterComponent @bind-Counter="@ViewModel.Counter" />

Benefits:

  • βœ… Zero Configuration - No setup required, works automatically
  • βœ… No Boilerplate - Eliminates 20+ lines of event handling code per component
  • βœ… Memory Safe - Automatic subscription cleanup prevents memory leaks
  • βœ… Convention-Based - Follows Blazor's standard @bind- naming pattern
  • βœ… Type-Safe - Compile-time checking for parameter types
  • βœ… Works Everywhere - Supported in MvvmComponentBase, MvvmOwningComponentBase, and MvvmLayoutComponentBase

The feature automatically detects matching EventCallback parameters and wires them up during component initialization, with proper disposal when the component is removed.

Note

For a complete working demonstration of Parameter Resolution and Automatic Two-Way Binding, see the ParameterResolution sample page in most Sample Projects.

MVVM Navigation

No more magic strings! Strongly-typed navigation is now possible. If the page URI changes, you no longer need to search through your source code to make updates. It is auto-magically resolved at runtime for you!

When the MvvmNavigationManager is initialized by the IOC container as a Singleton, the class examines all assemblies and internally caches all ViewModels (classes and interfaces) along with their associated pages.

When navigation is required, a quick lookup is performed, and the Blazor NavigationManager is used to navigate to the correct page. Any relative URI or query string passed via the NavigateTo method call is also included.

Note

The MvvmNavigationManager class is not a complete replacement for the Blazor NavigationManager class; it only adds support for MVVM.

Modify the NavMenu.razor to use MvvmNavLink:

<div class="nav-item px-3">
    <MvvmNavLink class="nav-link" TViewModel="FetchDataViewModel">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
    </MvvmNavLink>
</div>

Note

The MvvmNavLink component is based on the Blazor NavLink component and includes additional TViewModel and RelativeUri properties. Internally, it uses the MvvmNavigationManager for navigation.

Navigate by ViewModel using the MvvmNavigationManager from code:

Inject the MvvmNavigationManager class into your page or ViewModel, then use the NavigateTo method:

mvvmNavigationManager.NavigateTo<FetchDataViewModel>();

The NavigateTo method works the same as the standard Blazor NavigationManager and also supports passing a relative URL and/or query string.

Navigate by abstraction

If you prefer abstraction, you can also navigate by interface as shown below:

mvvmNavigationManager.NavigateTo<ITestNavigationViewModel>();

The same principle works with the MvvmNavLink component:

<div class="nav-item px-3">
    <MvvmNavLink class="nav-link"
                 TViewModel=ITestNavigationViewModel
                 Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span>Test
    </MvvmNavLink>
</div>
<div class="nav-item px-3">
    <MvvmNavLink class="nav-link"
                 TViewModel=ITestNavigationViewModel
                 RelativeUri="this is a MvvmNavLink test"
                 Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span>Test + Params
    </MvvmNavLink>
</div>
<div class="nav-item px-3">
    <MvvmNavLink class="nav-link"
                 TViewModel=ITestNavigationViewModel
                 RelativeUri="?test=this%20is%20a%20MvvmNavLink%20querystring%20test"
                 Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span>Test + QueryString
    </MvvmNavLink>
</div>
<div class="nav-item px-3">
    <MvvmNavLink class="nav-link"
                 TViewModel=ITestNavigationViewModel
                 RelativeUri="this is a MvvmNvLink test/?test=this%20is%20a%20MvvmNavLink%20querystring%20test"
                 Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span>Test + Both
    </MvvmNavLink>
</div>

Navigate by ViewModel Key using the MvvmNavigationManager from code:

Inject the MvvmNavigationManager class into your page or ViewModel, then use the NavigateTo method:

MvvmNavigationManager.NavigateTo("FetchDataViewModel");

The same principle works with the MvvmKeyNavLink component:

<div class="nav-item px-3">
    <MvvmKeyNavLink class="nav-link"
                    NavigationKey="@nameof(TestKeyedNavigationViewModel)"
                    Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span> Keyed Test
    </MvvmKeyNavLink>
</div>

<div class="nav-item px-3">
    <MvvmKeyNavLink class="nav-link"
                    NavigationKey="@nameof(TestKeyedNavigationViewModel)"
                    RelativeUri="this is a MvvmKeyNavLink test"
                    Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span> Keyed + Params
    </MvvmKeyNavLink>
</div>

<div class="nav-item px-3">
    <MvvmKeyNavLink class="nav-link"
                    NavigationKey="@nameof(TestKeyedNavigationViewModel)"
                    RelativeUri="?test=this%20is%20a%20MvvmKeyNavLink%20querystring%20test"
                    Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span> Keyed + QueryString
    </MvvmKeyNavLink>
</div>

<div class="nav-item px-3">
    <MvvmKeyNavLink class="nav-link"
                    NavigationKey="@nameof(TestKeyedNavigationViewModel)"
                    RelativeUri="this is a MvvmKeyNavLink test/?test=this%20is%20a%20MvvmKeyNavLink%20querystring%20test"
                    Match="NavLinkMatch.All">
        <span class="oi oi-calculator" aria-hidden="true"></span> Keyed + Both
    </MvvmKeyNavLink>
</div>

Navigation Fallback

MvvmNavigationManager still supports normal NavigationManager magic string navigation, as it is still used internally by MvvmNavigationManager.

MVVM Validation

The library provides an MvvmObservableValidator component that works with the EditForm component to enable validation using the ObservableValidator class from the CommunityToolkit.Mvvm library.

The following example demonstrates how to use the MvvmObservableValidator component with the EditForm component to perform validation.

First, define a class that inherits from the ObservableValidator class and contains properties with validation attributes:

public class ContactInfo : ObservableValidator
{
    private string? _name;

    [Required]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "The {0} field must have a length between {2} and {1}.")]
    [RegularExpression(@"^[a-zA-Z\s'-]+$", ErrorMessage = "The {0} field contains invalid characters. Only letters, spaces, apostrophes, and hyphens are allowed.")]
    public string? Name
    {
        get => _name;
        set => SetProperty(ref _name, value, true);
    }

    private string? _email;

    [Required]
    [EmailAddress]
    public string? Email
    {
        get => _email;
        set => SetProperty(ref _email, value, true);
    }

    private string? _phoneNumber;

    [Required]
    [Phone]
    [Display(Name = "Phone Number")]
    public string? PhoneNumber
    {
        get => _phoneNumber;
        set => SetProperty(ref _phoneNumber, value, true);
    }
}

Next, in the ViewModel component, define the property that will hold the object to be validated and the methods that will be called when the form is submitted:

public sealed partial class EditContactViewModel : ViewModelBase, IDisposable
{
    private readonly ILogger<EditContactViewModel> _logger;

    [ObservableProperty]
    private ContactInfo _contact = new();

    public EditContactViewModel(ILogger<EditContactViewModel> logger)
    {
        _logger = logger;
        Contact.PropertyChanged += ContactOnPropertyChanged;
    }

    public void Dispose()
        => Contact.PropertyChanged -= ContactOnPropertyChanged;

    [RelayCommand]
    private void ClearForm()
        => Contact = new ContactInfo();

    [RelayCommand]
    private void Save()
        => _logger.LogInformation("Form is valid and submitted!");

    private void ContactOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
        => NotifyStateChanged();
}

Finally, in the View component, use the EditForm component with the MvvmObservableValidator component to enable validation:

@page "/form"
@inherits MvvmComponentBase<EditContactViewModel>

<EditForm Model="ViewModel.Contact" FormName="EditContact" OnValidSubmit="ViewModel.SaveCommand.Execute">
    <MvvmObservableValidator />
    <ValidationSummary />

    <div class="row g-3">
        <div class="col-12">
            <label class="form-label">Name:</label>
            <InputText aria-label="name" @bind-Value="ViewModel.Contact.Name" class="form-control" placeholder="Some Name"/>
            <ValidationMessage For="() => ViewModel.Contact.Name" />
        </div>

        <div class="col-12">
            <label class="form-label">Email:</label>
            <InputText aria-label="email" @bind-Value="ViewModel.Contact.Email" class="form-control" placeholder="[email protected]"/>
            <ValidationMessage For="() => ViewModel.Contact.Email" />
        </div>
        <div class="col-12">
            <label class="form-label">Phone Number:</label>
            <InputText aria-label="phone number" @bind-Value="ViewModel.Contact.PhoneNumber" class="form-control" placeholder="555-1212"/>
            <ValidationMessage For="() => ViewModel.Contact.PhoneNumber" />
        </div>
    </div>

    <hr class="my-4">

    <div class="row">
        <button class="btn btn-primary btn-lg col"
                type="submit"
                disabled="@ViewModel.Contact.HasErrors">
        Save
        </button>
        <button class="btn btn-secondary btn-lg col"
                type="button" 
                @onclick="ViewModel.ClearFormCommand.Execute">
            Clear Form
        </button>
    </div>
</EditForm>  

Subpath Hosting

Blazing.Mvvm supports hosting your Blazor application under a subpath of a web server. This is useful when you want to serve your application from a specific URL segment rather than the domain root (e.g., https://example.com/myapp instead of https://example.com).

Automatic Base Path Detection (Recommended)

Note

Since v3.1.0, Blazing.Mvvm automatically detects the base path from NavigationManager.BaseUri. In most scenarios, including YARP reverse proxy setups, no manual BasePath configuration is required.

The base path is dynamically extracted at navigation time, making your application work seamlessly in:

  • Standard subpath hosting
  • YARP reverse proxy scenarios
  • Multi-tenant applications with dynamic paths
  • Development and production environments without configuration changes

Standard Subpath Hosting

For traditional subpath hosting (without YARP), configure your application as follows:

1. Configure launchSettings.json

Add the launchUrl property to specify the subpath:

{
  "profiles": {
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "fu/bar",
      "applicationUrl": "https://localhost:7037;http://localhost:5272",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

2. Configure ASP.NET Core Middleware in Program.cs

app.UsePathBase("/fu/bar/");
app.UseRouting();

3. Update _Host.cshtml (legacy) or App.razor for dynamic base href

You can hard-code the path, eg: <base href="/fu/bar/" />, however, it's better to set it dynamically based on the incoming request's PathBase.

Host.cshtml (Razor Pages) Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <base href="@baseHref" />
    <!-- rest of head -->
</head>

@{
    var baseHref = HttpContext?.Request?.PathBase.HasValue == true
        ? HttpContext?.Request.PathBase.Value!.TrimEnd('/') + "/"
        : "/";
}

App. razor (Razor Components) Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <base href="@baseHref" />
    <!-- rest of head -->
</head>

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    private string baseHref => HttpContext?.Request.PathBase.HasValue == true
        ? HttpContext.Request.PathBase.Value!.TrimEnd('/') + "/"
        : "/";
}

4. Configure Blazing.Mvvm (No BasePath needed)

builder.Services.AddMvvm(options =>
{
    options.HostingModelType = BlazorHostingModelType.Server;
    options.ParameterResolutionMode = ParameterResolutionMode.ViewAndViewModel;
    // BasePath is automatically detected - no configuration needed!
});

YARP (Yet Another Reverse Proxy) Support

YARP scenarios are automatically supported. When YARP sets the PathBase on incoming requests, Blazing.Mvvm automatically detects and uses it for navigation.

1. Configure YARP in appsettings.json

{
  "ReverseProxy": {
    "Routes": {
      "blazor-route": {
        "ClusterId": "blazor-cluster",
        "Match": {
          "Path": "/fu/bar/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/fu/bar" }
        ]
      }
    },
    "Clusters": {
      "blazor-cluster": {
        "Destinations": {
          "blazor-destination": {
            "Address": "http://localhost:5005/"
          }
        }
      }
    }
  }
}

2. Configure YARP in Program.cs

// Enable forwarded headers support
app.UseForwardedHeaders();

// Optional: Handle X-Forwarded-Prefix header for custom YARP configurations
app.Use((ctx, next) =>
{
    if (ctx.Request.Headers.TryGetValue("X-Forwarded-Prefix", out StringValues prefix) &&
        !StringValues.IsNullOrEmpty(prefix))
    {
        var p = prefix.ToString();
        if (!string.IsNullOrEmpty(p))
            ctx.Request.PathBase = p;  
    }
    return next();
});

// For testing/development: Force a specific base path
app.Use((ctx, next) =>
{
    ctx.Request.PathBase = "/fu/bar"; 
    return next();
});

3. Update _Host.cshtml (legacy) or App.razor for dynamic base href

Do not hard-code the path. Yarp will use a dynamic PathBase for baseHref, so set it based on the incoming request's PathBase.

Host.cshtml (Razor Pages) Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <base href="@baseHref" />
    <!-- rest of head -->
</head>

@{
    var baseHref = HttpContext?.Request?.PathBase.HasValue == true
        ? HttpContext?.Request.PathBase.Value!.TrimEnd('/') + "/"
        : "/";
}

App. razor (Razor Components) Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <base href="@baseHref" />
    <!-- rest of head -->
</head>

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    private string baseHref => HttpContext?.Request.PathBase.HasValue == true
        ? HttpContext.Request.PathBase.Value!.TrimEnd('/') + "/"
        : "/";
}

4. Configure Blazing.Mvvm (No BasePath needed)

builder.Services.AddMvvm(options =>
{
    options.HostingModelType = BlazorHostingModelType.Server;
    options.ParameterResolutionMode = ParameterResolutionMode.ViewAndViewModel;
    // BasePath is automatically detected from YARP's PathBase!
});

Legacy Configuration (Backward Compatible)

If you need to explicitly override the detected base path, you can still set the BasePath property (marked as [Obsolete] but fully functional):

Configure Blazing.Mvvm in Program.cs

builder.Services.AddMvvm(options =>
{
    options.HostingModelType = BlazorHostingModelType.Server;
    options.ParameterResolutionMode = ParameterResolutionMode.ViewAndViewModel;
    options.BasePath = "/fu/bar/"; // Optional override - typically not needed
});

Configure ASP.NET Core Middleware

app.UsePathBase("/fu/bar/");
app.UseRouting();

Set static base href

<base href="/fu/bar/" />

Configuration Priority

The base path resolution follows this priority order:

  1. Configured BasePath (if explicitly set in AddMvvm options)
  2. Dynamic detection from NavigationManager.BaseUri (recommended)

This ensures backward compatibility while enabling zero-configuration for most scenarios.

Working Examples

For complete working examples, see:

Further Reading

For more information about ASP.NET Core subpath hosting and YARP configuration, see:

Supported Navigation Route Patterns

Blazing.Mvvm supports a comprehensive set of route patterns for flexible navigation in your Blazor applications. All patterns work seamlessly with both type-based navigation (NavigateTo<TViewModel>) and keyed navigation (NavigateTo(key)).

Simple Routes

Navigate to pages with simple, static routes:

// Page with @page "/"
mvvmNavigationManager.NavigateTo<HomeViewModel>();

// Page with @page "/counter"
mvvmNavigationManager.NavigateTo<CounterViewModel>();

// Page with @page "/fetchdata"
mvvmNavigationManager.NavigateTo<FetchDataViewModel>();

Single Parameter Routes

Navigate to pages with a single route parameter:

// Page with @page "/users/{userId}"
mvvmNavigationManager.NavigateTo<UserViewModel>("123");
// Results in: /users/123

// Page with @page "/products/{productId}"
mvvmNavigationManager.NavigateTo<ProductViewModel>("abc-456");
// Results in: /products/abc-456

Multiple Parameter Routes

Navigate to pages with two or more route parameters:

// Page with @page "/users/{userId}/posts/{postId}"
mvvmNavigationManager.NavigateTo<UserPostViewModel>("1/101");
// Results in: /users/1/posts/101

// Page with @page "/api/{version}/users/{userId}/posts/{postId}"
mvvmNavigationManager.NavigateTo<ApiUserPostViewModel>("v2/1/101");
// Results in: /api/v2/users/1/posts/101

Pattern Rules:

  • Parameters are separated by forward slashes (/)
  • The order of parameters must match the route template
  • Supports any number of route parameters

Query String Support

Add query strings to any navigation:

// Simple query string
mvvmNavigationManager.NavigateTo<ProductsViewModel>("?category=electronics");
// Results in: /products?category=electronics

// Multiple query parameters
mvvmNavigationManager.NavigateTo<SearchViewModel>("?query=blazor&sort=relevance&page=1");
// Results in: /search?query=blazor&sort=relevance&page=1

Combined Parameters and Query Strings

Combine route parameters with query strings:

// Single parameter + query string
// Page with @page "/users/{userId}"
mvvmNavigationManager.NavigateTo<UserViewModel>("123?tab=profile&edit=true");
// Results in: /users/123?tab=profile&edit=true

// Multiple parameters + query string
// Page with @page "/users/{userId}/posts/{postId}"
mvvmNavigationManager.NavigateTo<UserPostViewModel>("1/101?filter=recent&sort=desc");
// Results in: /users/1/posts/101?filter=recent&sort=desc

// Complex multi-level route + query string
// Page with @page "/api/{version}/users/{userId}/posts/{postId}"
mvvmNavigationManager.NavigateTo<ApiUserPostViewModel>("v2/1/101?include=comments&expand=author");
// Results in: /api/v2/users/1/posts/101?include=comments&expand=author

Complex Multi-Level Routes

Navigate to deeply nested routes with multiple segments:

// Page with @page "/admin/settings/users/{userId}/permissions"
mvvmNavigationManager.NavigateTo<UserPermissionsViewModel>("123");
// Results in: /admin/settings/users/123/permissions

// Page with @page "/app/tenant/{tenantId}/workspace/{workspaceId}/project/{projectId}"
mvvmNavigationManager.NavigateTo<ProjectViewModel>("abc/ws-123/proj-456");
// Results in: /app/tenant/abc/workspace/ws-123/project/proj-456

Pattern Rules:

  • βœ… Route parameters are defined with curly braces: {paramName}
  • βœ… Parameters are substituted in order from the relativeUri string
  • βœ… Query strings start with ? and use & to separate multiple parameters
  • βœ… URL encoding is handled automatically by the navigation manager
  • βœ… Works with subpath hosting and YARP reverse proxy scenarios
  • βœ… Supports dynamic base path detection (no manual configuration needed)

Working Examples:

For complete working examples demonstrating these route patterns, see Sample Projects.

Complex Multi-Project ViewModel Registration

When working with complex multi-project solutions where ViewModels are distributed across multiple assemblies, you can register all ViewModels from different assemblies using the RegisterViewModelsFromAssemblyContaining method in the AddMvvm configuration.

This is particularly useful in Hybrid applications (WPF, WinForms, MAUI, Avalonia) where you might have:

  • A core project containing business logic and ViewModels
  • A Blazor UI project containing page-specific ViewModels
  • Shared ViewModels across multiple projects

Example: Registering ViewModels from Multiple Assemblies

using Blazing.Mvvm;
using HybridSample.Core.ViewModels;
using HybridSample.Blazor.Core.Pages;

builder.Services.AddMvvm(options =>
{ 
    options.HostingModelType = BlazorHostingModelType.Hybrid;
    
    // Register ViewModels from the Core project
    options.RegisterViewModelsFromAssemblyContaining<SamplePageViewModel>();
    
    // Register ViewModels from the Blazor.Core project
    options.RegisterViewModelsFromAssemblyContaining<IntroductionPage>();
});

Alternative Methods

You can also register assemblies directly:

// Using Type
var coreAssembly = typeof(SamplePageViewModel).Assembly;
var blazorAssembly = typeof(IntroductionPage).Assembly;

builder.Services.AddMvvm(options =>
{ 
    options.RegisterViewModelsFromAssembly(coreAssembly, blazorAssembly);
});

// Or using RegisterViewModelsFromAssemblies for a collection
var assemblies = new[] { coreAssembly, blazorAssembly };
builder.Services.AddMvvm(options =>
{ 
    options.RegisterViewModelsFromAssemblies(assemblies);
});

This approach ensures that all ViewModels across your solution are properly discovered and registered with the dependency injection container, enabling seamless MVVM navigation and component resolution.

For working examples, see the Hybrid sample projects:

Sample Projects

The repository includes several sample projects demonstrating different Blazor hosting models and scenarios. As of February 2, 2026, all Blazor MVVM samples have been refactored to use a centralized Blazing.Mvvm.Sample.Shared project, demonstrating best practices for code sharing across different hosting models.

Blazor Hosting Model Samples

All of the following samples now reference the shared Blazing.Mvvm.Sample.Shared library, which contains common components, ViewModels, pages, and services:

Shared Content (Blazing.Mvvm.Sample.Shared):

  • RelayCommand Examples - Comprehensive demonstrations of synchronous and asynchronous command patterns, including AllowConcurrentExecutions behavior
  • Parameter Resolution - Automatic two-way binding with @bind- syntax (integrated from ParameterResolution.Sample.Wasm)
  • Parent-Child Communication - Messenger-based component communication patterns (integrated from Blazing.Mvvm.ParentChildSample)
  • Bootstrap Components - Reusable Bootstrap 5 wrapper components
  • MVVM Validation - Form validation with ObservableValidator
  • Multi-Parameter Routing - Complex route patterns with multiple parameters and query strings

Blazor Hybrid Samples

Modernises Microsoft's Xamarin Sample project, using Blazing.Mvvm, for the CommunityToolkit.Mvvm. Minimal changes were made.

NOTE: The original Project was Blazor MVVM Sample - now archived.

Specialized Samples

Moved (Archived) Samples

  • ParameterResolution.Sample.Wasm - (Archived - now integrated into Blazing.Mvvm.Sample.Shared) Demonstrates parameter resolution between Views and ViewModels using ViewParameter attribute, and automatic two-way binding with @bind- syntax
  • Blazing.Mvvm.ParentChildSample - (Archived - now integrated into Blazing.Mvvm.Sample.Shared) Demonstrates dynamic parent-child component communication using Messenger. Original repo is now archived.

Component Libraries

The sample projects include several reusable component libraries that demonstrate MVVM patterns and best practices:

MvvmButton (Blazing.Buttons)
  • MVVM-aware button component with integrated command binding
  • Automatic disabled state management when commands cannot execute
  • Seamless integration with IRelayCommand and IAsyncRelayCommand
  • Example usage in all sample applications
Bootstrap Components (Blazing.Mvvm.Sample.Shared/Components/Bootstrap)

Production-ready Bootstrap 5 wrapper components demonstrating component composition patterns:

  • BootstrapAccordion & BootstrapAccordionItem - Collapsible content panels with Bootstrap styling
  • BootstrapBreadcrumbs - Navigation breadcrumb trails with MVVM-friendly API
  • BootstrapCard - Content containers with headers, footers, and customizable styling
  • BootstrapNavMenu & BootstrapNavMenuGroup - Hierarchical navigation menus with collapsible groups and JavaScript interop
  • BootstrapRowGroup & BootstrapRowGroupItem - Grouped row layouts for structured content display
Blazor Common Utilities (Blazing.Common)

Shared utility components and helpers used across sample projects:

  • ConditionalSwitch, When, Otherwise - Declarative conditional rendering components (alternative to if/else in markup)
  • ComponentControlBase, ComponentInputControlBase - Base classes for reusable components

These component libraries are included in the sample projects to demonstrate:

  • How to build reusable, MVVM-aware components
  • Component composition and communication patterns
  • Integration with popular CSS frameworks (Bootstrap 5)
  • Code organization and architectural patterns

Running Samples with Different .NET Target Frameworks

All sample projects in this repository support multi-targeting across .NET 8, .NET 9, and .NET 10. To run a sample with a specific .NET version:

  1. Open the solution in Visual Studio or your preferred IDE
  2. Right-click on the sample project you want to run and Set as Startup Project
  3. Select the Start With Debugging Run Button (green solid) dropdown arrow
  4. Select the target framework from the dropdown (e.g., net8.0, net9.0, net10.0)
  5. Run the project

For detailed instructions on switching between .NET target frameworks and troubleshooting multi-targeting scenarios, see the Running Samples with Different .NET Versions guide.

History

V3.2.1 - 2 February 2026

This maintenance release focuses on improvements to the sample project and bug fixes.

Improvements:

  • IAsyncRelayCommand Edge Case Fix: (Issue #65) Improved support for edge cases where PropertyChanged events were blocked when ExecutionTask is awaited, particularly when AllowConcurrentExecutions is set to false. This ensures UI updates propagate correctly even when awaiting long-running async commands. @gragra33 & @teunlielu

Warning

Updates to ViewModelBase and ValidatorViewModelBase now implement IDisposable for PropertyChanged event tracking. This may cause build errors when IDisposable is implemented manually. Use protected override void Dispose(bool disposing) to handle manual disposal in derived classes.

Sample Project Refactoring:

  • Major Consolidation: Refactored Blazing.Mvvm.Sample.Server, Blazing.Mvvm.Sample.Wasm, Blazing.Mvvm.Sample.WebApp, Blazing.Mvvm.Sample.HybridMaui, and Blazing.SubpathHosting.Server to use a centralized Blazing.Mvvm.Sample.Shared library. @gragra33
  • Integrated Standalone Samples: Moved content from ParameterResolution.Sample.Wasm and Blazing.Mvvm.ParentChildSample into the shared library, making these patterns available across all sample applications. @gragra33
  • New RelayCommand Sample Page: Added comprehensive RelayCommands page demonstrating synchronous and asynchronous command patterns, AllowConcurrentExecutions behavior, command parameters, and CanExecute validation. @gragra33

Component Libraries:

  • MvvmButton Component: New MVVM-aware button component (Blazing.Buttons) with integrated command binding and automatic state management. @gragra33
  • Bootstrap Components: Added production-ready Bootstrap 5 wrapper components, including BootstrapAccordion, BootstrapBreadcrumbs, BootstrapCard, BootstrapNavMenu, and BootstrapRowGroup to Blazing.Mvvm.Sample.Shared. @gragra33
  • ConditionalSwitch Component: Added declarative conditional rendering components (ConditionalSwitch, When, Otherwise) to Blazing.Common library. @gragra33

Documentation:

  • Updated Blazing.SubpathHosting.Server readme with comprehensive information about sample architecture, component libraries, and recent updates. @gragra33
  • Added reference to Subpath_Hosting_Guidance.md for detailed subpath hosting best practices. @gragra33

Benefits of Refactoring:

  • Demonstrates best practices for code sharing across Blazor hosting models (Server, WebAssembly, Web App, Hybrid MAUI)
  • Reduces code duplication and maintenance overhead
  • Provides consistent examples across all hosting models
  • Easier to add new features that work everywhere

V3.2.0 - 7 January 2026

This release adds support for:

  • automatic two-way binding support, eliminating the need for manual PropertyChanged event handling in components. @gragra33
  • complex route patterns with multiple parameters and query strings. @gragra33

New Features:

  • Automatic Two-Way Binding: Components with EventCallback<T> parameters following the {PropertyName}Changed convention and corresponding [ViewParameter] properties in ViewModels now automatically wire up two-way binding. @gragra33
  • Multi-Parameter Route Support: Full support for routes with multiple parameters (e.g., /users/{userId}/posts/{postId}).
  • Enhanced Route Parameter Substitution: Smart substitution of route parameters with proper URL encoding and query string handling.
  • Combined Parameters + Query Strings: Navigate with both route parameters and query strings in a single call (e.g., 1/101?filter=recent&sort=desc).
  • Complex Multi-Level Routes: Support for deeply nested routes with multiple segments and parameters.

New Sample:

  • ParameterResolution.Sample.Wasm - Demonstrates parameter resolution between Views and ViewModels using ViewParameter attribute, and automatic two-way binding with @bind- syntax

Updated Samples:

  • Updated sample projects to demonstrate complex route patterns:
    • Blazing.Mvvm.Sample.Server, Blazing.Mvvm.Sample.WebApp, Blazing.Mvvm.Sample.Wasm, Blazing.Mvvm.Sample.HybridMaui

V3.1.0 - 3 December 2025

This release adds automatic base path detection for YARP reverse proxy scenarios and simplifies configuration.

New Features:

  • Automatic Base Path Detection: Base path is now automatically detected from NavigationManager.BaseUri, eliminating the need for manual BasePath configuration in most scenarios. @gragra33 & @teunlielu
  • YARP Support: Full support for YARP (Yet Another Reverse Proxy) with automatic detection of dynamically assigned paths via PathBase. @gragra33 & @teunlielu
  • Dynamic Per-Request Base Paths: Supports scenarios where different requests have different base paths, ideal for multi-tenant applications. @gragra33 & @teunlielu

Improvements:

  • BasePath property is now marked as [Obsolete] but remains functional for backward compatibility. @gragra33
  • Added 15 new unit tests and integration tests for dynamic base path scenarios (total 867 tests). @gragra33
  • Enhanced logging for base path detection to aid in diagnostics. @gragra33
  • Updated documentation with YARP configuration examples and best practices. @gragra33
  • Updated Blazing.SubpathHosting.Server to support new base path detection features.@gragra33

Configuration:

  • No configuration required for most scenarios - base path is automatically detected
  • For YARP scenarios, simply use app.UseForwardedHeaders() and optionally handle X-Forwarded-Prefix header
  • Existing code using BasePath is now marked obsolete, but continues to work without changes. Will be removed in a future release.

V3.0.0 - 18 November 2025

This is a major release with new features and enhancements.

  • Added support for .NET 10. @gragra33
  • Added subpath hosting support for serving Blazor applications from URL subpaths. @gragra33
  • Added new sample projects:
    • Blazing.Mvvm.ParentChildSample - Demonstrates dynamic parent-child component communication
    • Blazing.SubpathHosting.Server - Demonstrates subpath hosting configuration
    • Hybrid samples for WinForms, WPF, MAUI, and Avalonia platforms
  • Added multi-targeting support across .NET 8, .NET 9, and .NET 10 for all sample projects. @gragra33
  • Increased test coverage with an additional 128 unit tests (total 208 tests). @gragra33
  • Enhanced documentation with comprehensive guides for:
    • Subpath hosting configuration
    • Complex multi-project ViewModel registration
    • Running samples with different .NET target frameworks
  • Documentation updates and improvements. @gragra33

About

πŸ”₯ Blazing.Mvvm - Full MVVM support for Blazor with CommunityToolkit.Mvvm integration. Supports all hosting models (Server, WASM, SSR, Auto, Hybrid, MAUI). Features strongly-typed navigation, automatic ViewModel registration, parameter resolution, validation support, and comprehensive lifecycle management. Includes samples and full documentation.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 5