Lifted.MVVM 8.0.0

dotnet add package Lifted.MVVM --version 8.0.0
                    
NuGet\Install-Package Lifted.MVVM -Version 8.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lifted.MVVM" Version="8.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lifted.MVVM" Version="8.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Lifted.MVVM" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lifted.MVVM --version 8.0.0
                    
#r "nuget: Lifted.MVVM, 8.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lifted.MVVM@8.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lifted.MVVM&version=8.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Lifted.MVVM&version=8.0.0
                    
Install as a Cake Tool

Lifted.MVVM

NuGet License

A lightweight .NET 8 Blazor MVVM framework that simplifies building Blazor applications using the Model-View-ViewModel pattern. Built on top of the CommunityToolkit.Mvvm, Lifted.MVVM provides ready-to-use base classes and components that eliminate boilerplate code and handle common MVVM patterns automatically.

🚀 Features

  • Zero-Configuration MVVM: Automatic property change notifications and UI updates
  • Multiple ViewModel Base Classes: Choose the right base for your needs (basic, validation, messaging)
  • Blazor Component Integration: Seamless integration with Blazor's component lifecycle
  • Built-in Validation Support: Easy form validation with visual feedback
  • Messaging Support: Inter-component communication using the Messenger pattern
  • Dependency Injection Ready: Full support for DI with automatic ViewModel injection
  • Async-First Design: Built-in async initialization support

📦 Installation

Install via NuGet Package Manager:

dotnet add package Lifted.MVVM

Or via Package Manager Console:

Install-Package Lifted.MVVM

🎯 Quick Start

1. Create a ViewModel

Choose one of three base classes depending on your needs:

using Lifted.MVVM.ComponentModel.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class CounterViewModel : LiftedVM
{
    [ObservableProperty]
    private int _count;

    [RelayCommand]
    private void IncrementCount()
    {
        Count++;
    }

    public override async Task Loaded()
    {
        // Initialize your ViewModel here
        Count = 0;
        await base.Loaded();
    }
}

2. Register the ViewModel

In your Program.cs:

builder.Services.AddScoped<CounterViewModel>();

3. Create a Blazor Component

@page "/counter"
@inherits LiftedComponentBase<CounterViewModel>

<h1>Counter</h1>

<p>Current count: @VM.Count</p>

<button @onclick="VM.IncrementCountCommand.Execute">Click me</button>

That's it! The component automatically re-renders when Count changes.

📚 Core Components

ViewModel Base Classes

LiftedVM

The basic ViewModel base class for most scenarios.

public partial class MyViewModel : LiftedVM
{
    [ObservableProperty]
    private string _message = "Hello, World!";

    public override async Task Loaded()
    {
        // Called when the component initializes
        await Task.CompletedTask;
    }
}

Features:

  • Inherits from ObservableObject (CommunityToolkit.Mvvm)
  • Automatic property change notifications
  • OnInitializedAsync() lifecycle method
  • Loaded() command for initialization logic
  • NotifyStateChanged() to trigger full UI refresh
LiftedValidatorVM

For ViewModels that require validation support.

using System.ComponentModel.DataAnnotations;

public partial class LoginViewModel : LiftedValidatorVM
{
    [ObservableProperty]
    [Required(ErrorMessage = "Username is required")]
    [MinLength(3, ErrorMessage = "Username must be at least 3 characters")]
    private string _username = string.Empty;

    [ObservableProperty]
    [Required(ErrorMessage = "Password is required")]
    private string _password = string.Empty;

    [RelayCommand]
    private async Task Login()
    {
        ValidateAllProperties();

        if (!HasErrors)
        {
            // Perform login
            await Task.CompletedTask;
        }
    }
}

Features:

  • Inherits from ObservableValidator (CommunityToolkit.Mvvm)
  • Built-in validation using Data Annotations
  • HasErrors property
  • GetErrors() method
  • Works with LiftedValidationSummary component
LiftedRecipientVM and LiftedRecipientVM<TMessage>

For ViewModels that need to send/receive messages between components.

// Define a message
public class UserLoggedInMessage
{
    public string Username { get; set; } = string.Empty;
}

// ViewModel that receives messages
public partial class HeaderViewModel : LiftedRecipientVM<UserLoggedInMessage>
{
    [ObservableProperty]
    private string _currentUser = "Guest";

    public override void Receive(UserLoggedInMessage message)
    {
        CurrentUser = message.Username;
    }

    public override async Task Loaded()
    {
        // Register to receive messages
        IsActive = true;
        await base.Loaded();
    }
}

// ViewModel that sends messages
public partial class LoginViewModel : LiftedRecipientVM
{
    [RelayCommand]
    private async Task Login(string username)
    {
        // Send message to other ViewModels
        Messenger.Send(new UserLoggedInMessage { Username = username });
        await Task.CompletedTask;
    }
}

Features:

  • Inherits from ObservableRecipient (CommunityToolkit.Mvvm)
  • Built-in messenger support
  • Type-safe message handling
  • Automatic message subscription/unsubscription

Blazor Component Base Classes

LiftedComponentBase<TViewModel>

The standard base class for Blazor pages and components.

@page "/mypage"
@inherits LiftedComponentBase<MyViewModel>

<h1>@VM.Title</h1>
<p>@VM.Description</p>

Features:

  • Automatic ViewModel injection via DI
  • Automatic UI updates on property changes
  • Calls VM.OnInitializedAsync() during component initialization
  • Access ViewModel via VM property
LiftedLayoutComponentBase<TViewModel>

For layout components that need a ViewModel.

@inherits LiftedLayoutComponentBase<LayoutViewModel>

<div class="page">
    <header>
        <h1>@VM.AppTitle</h1>
    </header>

    <main>
        @Body
    </main>
</div>

Features:

  • Same as LiftedComponentBase but for layouts
  • Inherits from LayoutComponentBase
LiftedParameterBoundComponentBase<TViewModel>

For child components that receive their ViewModel as a parameter instead of via DI.

@* Parent Component *@
<ChildComponent VM="@childViewModel" />

@* Child Component *@
@inherits LiftedParameterBoundComponentBase<ChildViewModel>

<div>
    <p>@VM.Message</p>
</div>

@code {
    // VM is received as a [Parameter] instead of [Inject]
}

Features:

  • ViewModel passed as [Parameter] instead of injected
  • Useful for reusable child components
  • Same automatic UI update behavior

Validation Component

LiftedValidationSummary

Displays validation errors from a LiftedValidatorVM.

@page "/register"
@inherits LiftedComponentBase<RegisterViewModel>

<EditForm Model="@VM" OnValidSubmit="@VM.RegisterCommand.Execute">
    <LiftedValidationSummary Context="@VM" />

    <InputText @bind-Value="VM.Username" />
    <InputText @bind-Value="VM.Email" type="email" />
    <InputText @bind-Value="VM.Password" type="password" />

    <button type="submit">Register</button>
</EditForm>

Features:

  • Automatically displays validation errors
  • Updates in real-time as errors change
  • Customizable CSS classes (validation-errors, validation-message)

💡 Usage Examples

Example 1: Layout with ViewModel

LayoutViewModel.cs

using Lifted.MVVM.ComponentModel.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class MainLayoutViewModel : LiftedVM
{
    [ObservableProperty]
    private string _appTitle = "My Application";

    [ObservableProperty]
    private string _currentUser = "Guest";

    public override async Task Loaded()
    {
        // Initialize layout data
        await Task.CompletedTask;
    }
}

MainLayout.razor

@inherits LiftedLayoutComponentBase<MainLayoutViewModel>

<div>
    <header>
        <h1>@VM.AppTitle</h1>
        <span>User: @VM.CurrentUser</span>
    </header>

    <main>
        @Body
    </main>

    <footer>
        <p>&copy; 2024 @VM.AppTitle</p>
    </footer>
</div>

Program.cs

builder.Services.AddScoped<MainLayoutViewModel>();

Example 2: Page with ViewModel

ProductsViewModel.cs

using Lifted.MVVM.ComponentModel.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class ProductsViewModel : LiftedVM
{
    [ObservableProperty]
    private List<Product> _products = new();

    [ObservableProperty]
    private string _searchTerm = string.Empty;

    [Inject]
    public IProductService ProductService { get; set; } = default!;

    public override async Task Loaded()
    {
        Products = await ProductService.GetAllAsync();
    }

    [RelayCommand]
    private async Task Search()
    {
        Products = await ProductService.SearchAsync(SearchTerm);
    }
}

Products.razor

@page "/products"
@inherits LiftedComponentBase<ProductsViewModel>

<h2>Products</h2>

<input @bind="VM.SearchTerm" placeholder="Search..." />
<button @onclick="VM.SearchCommand.Execute">Search</button>

<ul>
    @foreach (var product in VM.Products)
    {
        <li>@product.Name - $@product.Price</li>
    }
</ul>

Program.cs

builder.Services.AddScoped<ProductsViewModel>();
builder.Services.AddScoped<IProductService, ProductService>();

Example 3: Injected Component

AlertViewModel.cs

using Lifted.MVVM.ComponentModel.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class AlertViewModel : LiftedVM
{
    [ObservableProperty]
    private string _message = string.Empty;

    [ObservableProperty]
    private bool _isVisible = false;

    [RelayCommand]
    public void Show(string message)
    {
        Message = message;
        IsVisible = true;
    }

    [RelayCommand]
    public void Close()
    {
        IsVisible = false;
    }
}

AlertComponent.razor

@inherits LiftedComponentBase<AlertViewModel>

@if (VM.IsVisible)
{
    <div class="alert">
        <p>@VM.Message</p>
        <button @onclick="VM.CloseCommand.Execute">Close</button>
    </div>
}

Usage in a Page

@page "/home"
@inject AlertViewModel AlertVM

<h1>Home Page</h1>
<button @onclick="() => AlertVM.ShowCommand.Execute("Hello!")">Show Alert</button>

<AlertComponent />

Program.cs

builder.Services.AddScoped<AlertViewModel>();

Example 4: Parameter-Bound Component

CardViewModel.cs

using Lifted.MVVM.ComponentModel.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class CardViewModel : LiftedVM
{
    [ObservableProperty]
    private string _title = string.Empty;

    [ObservableProperty]
    private string _content = string.Empty;

    [ObservableProperty]
    private bool _isExpanded = false;

    [RelayCommand]
    private void ToggleExpand()
    {
        IsExpanded = !IsExpanded;
    }
}

Card.razor

@inherits LiftedParameterBoundComponentBase<CardViewModel>

<div class="card">
    <h3 @onclick="VM.ToggleExpandCommand.Execute">@VM.Title</h3>

    @if (VM.IsExpanded)
    {
        <p>@VM.Content</p>
    }
</div>

Usage in a Page

@page "/dashboard"

<h1>Dashboard</h1>

@foreach (var cardVM in cardViewModels)
{
    <Card VM="@cardVM" />
}

@code {
    private List<CardViewModel> cardViewModels = new()
    {
        new() { Title = "Card 1", Content = "Content 1" },
        new() { Title = "Card 2", Content = "Content 2" },
        new() { Title = "Card 3", Content = "Content 3" }
    };
}

🎨 Advanced Usage

Custom Initialization Logic

Override the Loaded() method to perform initialization:

public partial class ProductListViewModel : LiftedVM
{
    [ObservableProperty]
    private List<Product> _products = new();

    [Inject]
    public IProductService ProductService { get; set; } = default!;

    public override async Task Loaded()
    {
        Products = await ProductService.GetAllProductsAsync();
        await base.Loaded();
    }
}

Manual State Refresh

Force a complete UI refresh when needed:

public partial class DashboardViewModel : LiftedVM
{
    private async Task RefreshAll()
    {
        // Update multiple properties
        await LoadData();

        // Notify UI to refresh everything
        NotifyStateChanged();
    }
}

Combining Validation and Messaging

public partial class CheckoutViewModel : LiftedValidatorVM
{
    [Inject]
    public IMessenger Messenger { get; set; } = default!;

    [ObservableProperty]
    [Required]
    private string _shippingAddress = string.Empty;

    [RelayCommand]
    private async Task CompleteCheckout()
    {
        ValidateAllProperties();

        if (!HasErrors)
        {
            // Process checkout
            await ProcessOrder();

            // Notify other components
            Messenger.Send(new OrderCompletedMessage());
        }
    }
}

🔧 Configuration

Registering ViewModels

Register your ViewModels in Program.cs:

// Scoped (recommended for most scenarios)
builder.Services.AddScoped<MyViewModel>();

// Transient (new instance each time)
builder.Services.AddTransient<MyViewModel>();

// Singleton (shared across the app)
builder.Services.AddSingleton<MyViewModel>();

Using with Messenger

Register the messenger service:

builder.Services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);

📖 Best Practices

  1. Choose the Right Base Class

    • Use LiftedVM for basic scenarios
    • Use LiftedValidatorVM when you need validation
    • Use LiftedRecipientVM when you need messaging
  2. Lifecycle Management

    • Use Loaded() for async initialization
    • Don't perform heavy operations in constructors
    • Clean up resources in Dispose() if needed
  3. Property Changes

    • Use [ObservableProperty] for automatic notifications
    • Use NotifyStateChanged() sparingly for full refreshes
  4. Validation

    • Call ValidateAllProperties() before submitting forms
    • Check HasErrors before proceeding with operations
  5. Messaging

    • Set IsActive = true in Loaded() for recipients
    • Use specific message types for type safety
    • Remember to unregister when needed (handled automatically)

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

📞 Support


Made with ❤️ by CodeLifterIO

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.