ASP.NET Core Blazor 核心功能一:Blazor依赖注入与状态管理指南

简介: 本文由码农刚子撰写,系统介绍了Blazor框架中的依赖注入与状态管理。涵盖服务注册的三种生命周期、组件内状态、父子通信、级联参数、全局状态容器、Flux/Redux模式及本地存储持久化,并详解@ref的使用场景,为开发者提供全面的状态管理选型指南。

大家好,我是码农刚子本文详细介绍了Blazor框架中的依赖注入机制和状态管理方案。依赖注入部分阐述了服务注册的三种生命周期方式(Singleton/Scoped/Transient)及在组件中的使用方法。状态管理章节系统梳理了7种解决方案:从简单的组件内状态到父子组件通信、级联参数,再到全局状态容器和Flux/Redux模式,并提供了本地存储持久化方案。文章还介绍了@ref指令的使用场景,包括组件引用、元素操作和循环处理等。最后给出了不同场景下的状态管理选择建议,帮助开发者构建更健壮。

一、依赖注入基础

Blazor 提供了强大的依赖注入(Dependency Injection, DI)功能,用于将服务以解耦的方式注入到组件中,它帮助我们实现松耦合的代码设计,提高可测试性和可维护性。

什么是依赖注入?

依赖注入是一种设计模式,它允许类从外部接收其依赖项,而不是自己创建它们。在 Blazor 中,这意味着组件不需要知道如何创建服务,而是通过构造函数或属性接收这些服务。

二、注册和使用服务

1、创建自定义服务

1. 定义服务接口

public interface ICounterService
{
    int Increment(int currentValue);
    int Decrement(int currentValue);
    void Reset();
}

2. 实现服务

public class CounterService : ICounterService
{
    public int Increment(int currentValue)
    {
        return currentValue + 1;
    }
    
    public int Decrement(int currentValue)
    {
        return currentValue - 1;
    }
    
    public void Reset()
    {
        // 重置逻辑
    }
}

2、注册服务

Program.cs 文件中配置服务容器:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// 注册服务
builder.Services.AddSingleton<ICounterService, CounterService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddTransient<IEmailService, EmailService>();
// 注册内置服务
builder.Services.AddLocalStorage();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();

3、服务生命周期

Blazor 支持三种服务生命周期:

  • Singleton:整个应用生命周期内只有一个实例
  • Scoped:每个用户会话有一个实例(Blazor Server)或每个浏览器标签页(Blazor WebAssembly)
  • Transient:每次请求时创建新实例

4、在组件中使用依赖注入

1. 使用 [Inject] 特性

@page "/counter"
@inject ICounterService CounterService
<h3>Counter</h3>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    
    private void IncrementCount()
    {
        currentCount = CounterService.Increment(currentCount);
    }
}

2. 在代码中使用注入的服务

@page "/user-profile"
@inject IUserService UserService
@inject NavigationManager Navigation
<h3>User Profile</h3>
@if (user != null)
{
    <div>
        <p>Name: @user.Name</p>
        <p>Email: @user.Email</p>
    </div>
}
@code {
    private User user;
    
    protected override async Task OnInitializedAsync()
    {
        user = await UserService.GetCurrentUserAsync();
    }
    
    private async Task UpdateProfile()
    {
        await UserService.UpdateUserAsync(user);
        Navigation.NavigateTo("/success");
    }
}

5、高级依赖注入用法

1. 工厂模式注册

builder.Services.AddSingleton<ICounterService>(provider =>
{
    // 复杂的创建逻辑
    return new CounterService();
});

2. 选项模式

// 配置选项类
public class ApiOptions
{
    public string BaseUrl { get; set; }
    public int TimeoutSeconds { get; set; }
}
// 注册选项
builder.Services.Configure<ApiOptions>(options =>
{
    options.BaseUrl = "https://api.example.com";
    options.TimeoutSeconds = 30;
});
// 在服务中使用
public class ApiService
{
    private readonly ApiOptions _options;
    
    public ApiService(IOptions<ApiOptions> options)
    {
        _options = options.Value;
    }
}

3. 条件注册

#if DEBUG
builder.Services.AddSingleton<ILogger, DebugLogger>();
#else
builder.Services.AddSingleton<ILogger, ProductionLogger>();
#endif

三、组件状态管理

在Blazor开发中,状态管理是构建交互式Web应用的核心挑战。无论是简单的计数器组件还是复杂的实时协作系统,选择合适的状态管理方案直接影响应用性能和可维护性。

1、理解Blazor中的状态管理

  • 状态是指应用程序或组件在某一时刻的数据或信息。例如,一个计数器组件可以有一个表示当前计数值的状态,一个表单组件可以有一个表示用户输入的状态,一个购物车组件可以有一个表示选中商品的状态等。状态管理是指如何存储、更新、获取和传递这些数据或信息。
  • 在Blazor中,每个组件都有自己的私有状态,它只能被该组件访问和修改。如果要将状态从一个组件传递给另一个组件,或者在多个组件之间共享状态,就需要使用一些技术或模式来实现。下面我们将介绍一些常见的方法。

2、组件内状态:最简单的状态管理

Blazor组件最基础的状态管理方式是使用组件内部的字段或属性保存状态。这种模式适用于状态仅在单个组件内部使用且无需共享的场景,如计数器、表单输入等基础交互。

@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}

上述代码展示了典型的组件内状态模式,currentCount字段存储计数器状态,IncrementCount方法修改状态并自动触发UI重新渲染。这种模式的优势在于实现简单、零外部依赖,适合快速开发独立功能组件。

3、父子组件通信:参数和事件回调

如果要将父组件的状态传递给子组件,或者从子组件获取更新后的状态,可以使用参数属性来实现。

参数是指父组件向子组件传递数据或信息的方式。参数可以是任意类型的值,例如字符串、数字、布尔值、对象、委托等。要定义一个参数,需要在子组件中使用[Parameter]特性来标记一个公共属性,并且该属性的类型必须与父组件传递的值相同。例如:

这样就定义了一个名为Counter的参数,在子组件中可以使用以下语法来获取它的值:

<p>The counter value is @Counter</p>

在父组件中,可以使用以下语法来为参数赋值:

<CounterComponent Counter="@currentCount" />
@code {
  private int currentCount = 0;
}

这样就将父组件中的变量currentCount作为参数值传递给了子组件。如果要实现从父到子单向绑定。

属性是指子组件向父组件传递数据或信息的方式。属性可以是任意类型的值,但通常是一个事件回调(EventCallback)或一个动作(Action),用于在子组件中触发父组件定义的一个方法,从而将数据或信息传递给父组件。要定义一个属性,需要在子组件中使用[Parameter]特性来标记一个公共属性,并且该属性的类型必须是EventCallback<T>Action<T>,其中T是要传递的数据或信息的类型。例如:

<h3>CounterComponent</h3>
<p>The counter value is @Counter</p>
<button @onclick="CounterChangedFromChild">Update Counter from Child</button>
@code {
    [Parameter]
    public int Counter { get; set; }
    [Parameter]
    public EventCallback<int> OnCounterChanged { get; set; }
    private async Task CounterChangedFromChild()
    {
        Counter++;
        await OnCounterChanged.InvokeAsync(Counter);    
    }
}

以上例子中就定义了一个名为OnCounterChanged的属性,将子组件中的变量Counter作为参数传递给了父组件。在父组件中,可以使用以下语法来为属性赋值:

<CounterComponent OnCounterChanged="HandleCounterChanged" />

这样就将父组件中定义的一个方法名作为属性值传递给了子组件。该方法必须接受一个与属性类型相同的参数,并且可以在其中处理数据或信息。例如:

@code{
  private void HandleCounterChanged(int counter)
  {
    Console.WriteLine($"The counter value is {counter}");
  }
}

这样就实现了从子到父单向传递数据或信息,并且可以在任何时候触发。

使用组件参数和属性传递状态:适合父子组件之间的简单状态传递,可以使用[Parameter]或者级联参数[CascadingParameter]特性来标记组件参数,并且使用<Component Parameter="Value" />或者<CascadingValue Value="Value"><Component /></CascadingValue>语法来传递状态。

4、级联参数和值

<!-- AppStateProvider.razor -->
<CascadingValue Value="this">
    @ChildContent
</CascadingValue>
@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
    private string theme = "light";
    public string Theme
    {
        get => theme;
        set
        {
            if (theme != value)
            {
                theme = value;
                StateHasChanged();
            }
        }
    }
    public event Action? OnThemeChanged;
    public void ToggleTheme()
    {
        Theme = Theme == "light" ? "dark" : "light";
        OnThemeChanged?.Invoke();
    }
}
<!-- ConsumerComponent.razor -->
<div class="@($"app-{appState.Theme}")">
    <h3>当前主题: @appState.Theme</h3>
    <button @onclick="appState.ToggleTheme">切换主题</button>
</div>
@code {
    [CascadingParameter]
    public AppStateProvider appState { get; set; } = default!;
    protected override void OnInitialized()
    {
        if (appState != null)
        {
            appState.OnThemeChanged += StateHasChanged;
        }
    }
    public void Dispose()
    {
        if (appState != null)
        {
            appState.OnThemeChanged -= StateHasChanged;
        }
    }
}

5、状态容器模式(全局状态)

创建状态容器服务

// Services/AppState.cs
public class AppState
{
    private int _counter;
    private string _userName = string.Empty;
    public int Counter
    {
        get => _counter;
        set
        {
            _counter = value;
            OnCounterChanged?.Invoke();
        }
    }
    public string UserName
    {
        get => _userName;
        set
        {
            _userName = value;
            OnUserNameChanged?.Invoke();
        }
    }
    public event Action? OnCounterChanged;
    public event Action? OnUserNameChanged;
    public void IncrementCounter()
    {
        Counter++;
    }
}

注册服务

// Program.cs
builder.Services.AddScoped<AppState>();

在组件中使用

@inject AppState AppState
@implements IDisposable
<h3>计数器: @AppState.Counter</h3>
<h4>用户: @AppState.UserName</h4>
<button @onclick="AppState.IncrementCounter">增加计数</button>
<input @bind="localUserName" @bind:event="onchange" placeholder="输入用户名" />
@code {
    private string localUserName
    {
      get => AppState.UserName;
      set
      {
        AppState.UserName = value;
        // 可以在这里添加其他逻辑
      }
    }
    protected override void OnInitialized()
    {
        AppState.OnCounterChanged += StateHasChanged;
        AppState.OnUserNameChanged += StateHasChanged;
    }
    public void Dispose()
    {
        AppState.OnCounterChanged -= StateHasChanged;
        AppState.OnUserNameChanged -= StateHasChanged;
    }
}

6、Flux/Redux 模式

什么是Flux模式?

Flux是一种应用程序架构模式,专门用于管理前端应用中的状态。与常见的MVC模式不同,Flux采用单向数据流的设计,使得状态变化更加可预测和易于追踪。

Flux模式的核心思想是将状态管理与UI渲染分离,通过严格的规则来规范状态变更的过程。这种模式最初由Facebook提出,后来被Redux等库实现,而Fluxor则是专门为Blazor应用设计的实现方案。

Flux模式的核心原则

  1. 状态只读原则

应用的状态在任何情况下都不应该被直接修改,这保证了状态变更的可控性。

  1. 动作驱动变更

任何状态变更都必须通过派发(dispatch)一个动作(action)来触发。动作是一个简单的对象,描述了发生了什么变化。

  1. 纯函数处理

使用称为"reducer"的纯函数来处理动作,根据当前状态和动作生成新状态。Reducer不会修改原有状态,而是返回全新的状态对象。

  1. 单向数据流

UI组件订阅状态变化,当状态更新时自动重新渲染。用户交互则通过派发动作来触发状态变更,形成完整的单向循环。

核心概念

  • 状态(State)‌:定义应用数据模型,不可直接修改,需通过动作(Action)触发更新。
  • 动作(Action)‌:描述状态变更意图的对象,包含类型标识和有效载荷。
  • 归约器(Reducer)‌:纯函数,根据当前状态和动作生成新状态。
  • 效果(Effect)‌:处理副作用操作(如 API 调用),监听动作并执行异步任务。
  • 中间件(Middleware)中间件可以在动作被派发到reducer之前或之后执行自定义逻辑,用于日志记录、性能监控等横切关注点。

使用 Fluxor 库

首先安装 Fluxor:

Install-Package Fluxor.Blazor.Web

定义状态和动作

// Store/CounterState.cs
public record CounterState
{
    public int Count { get; init; }
}
// Store/Actions/IncrementCounterAction.cs
public record IncrementCounterAction;
// Store/Reducers/CounterReducers.cs
public static class CounterReducers
{
    [ReducerMethod]
    public static CounterState OnIncrementCounter(CounterState state, IncrementCounterAction action)
    {
        return state with { Count = state.Count + 1 };
    }
}

在组件中使用

@using Fluxor
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
<h3>计数器: @State.Value.Count</h3>
<button @onclick="Increment">增加</button>
@code {
    [Inject]
    private IState<CounterState> State { get; set; } = null!;
    [Inject]
    private IDispatcher Dispatcher { get; set; } = null!;
    private void Increment()
    {
        Dispatcher.Dispatch(new IncrementCounterAction());
    }
}

7、本地存储持久化

使用 Blazor 本地存储

@page "/counter2"
@inject IJSRuntime JSRuntime
<h3>持久化计数器: @count</h3>
<button @onclick="Increment">增加并保存</button>
@code {
    private int count = 0;
    private bool isInitialized = false;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await LoadFromStorage();
            isInitialized = true;
            StateHasChanged(); // 确保在加载后更新UI
        }
    }
    private async Task Increment()
    {
        count++;
        await SaveToStorage();
        //StateHasChanged();
    }
    private async Task SaveToStorage()
    {
        if (isInitialized)
        {
            await JSRuntime.InvokeVoidAsync("localStorage.setItem", "counter", count);
        }
    }
    private async Task LoadFromStorage()
    {
        try
        {
            var savedCount = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "counter");
            if (int.TryParse(savedCount, out int result))
            {
                count = result;
            }
        }
        catch (Exception ex)
        {
            // 处理预渲染期间的 JS 互操作错误
            Console.WriteLine($"加载存储时出错: {ex.Message}");
        }
    }
}

8、状态管理选择指南

场景

推荐方案

说明

简单组件状态

组件内部状态

状态仅在单个组件内使用

父子组件通信

参数 + 事件回调

直接的组件层级关系

深层组件树

级联参数

避免层层传递参数

跨组件状态共享

状态容器服务

多个组件需要访问相同状态

复杂应用状态

Fluxor/Redux

大型应用,需要可预测的状态管理

持久化需求

本地存储 + 状态容器

需要跨会话保持状态

四、使用 @ref 引用组件

在 Blazor 中,@ref 指令用于获取对组件或 HTML 元素的引用,让你能够在代码中直接操作它们。以下是详细的使用方法:

1、引用组件

基本用法

<!-- MyComponent.razor -->
<h3>计数器: @count</h3>
<button @onclick="Increment">增加</button>
@code {
    private int count = 0;
    public void Increment()
    {
        count++;
        StateHasChanged();
    }
    public void Reset()
    {
        count = 0;
        StateHasChanged();
    }
}
<!-- ParentComponent.razor -->
@page "/parent"
<MyComponent @ref="myComponentRef" />
<button @onclick="ResetChild">重置子组件</button>
@code {
    private MyComponent? myComponentRef;
    private void ResetChild()
    {
        myComponentRef?.Reset();
    }
}

2、引用 HTML 元素

@page "/element-ref"
<input @ref="usernameInput" placeholder="输入用户名" />
<button @onclick="FocusInput">聚焦输入框</button>
@code {
    private ElementReference usernameInput;
    private async Task FocusInput()
    {
        await usernameInput.FocusAsync();
    }
}

3、在循环中使用 @ref

@page "/loop-ref-example"
<h3>循环中使用 ref 示例</h3>
<button @onclick="ShowAllMessages" class="btn btn-primary">显示所有消息</button>
<button @onclick="UpdateAllMessages" class="btn btn-secondary">更新所有消息</button>
@foreach (var item in items)
{
  <ChildComponent @ref="componentRefs[item.Id]"
          Id="item.Id"
          Message="@item.Message"
          OnMessageChanged="HandleMessageChanged" />
}
@code {
  private List<ItemModel> items = new();
  private Dictionary<int, ChildComponent?> componentRefs = new();
  protected override void OnInitialized()
  {
    items = new List<ItemModel>
    {
      new ItemModel { Id = 1, Message = "第一条消息" },
      new ItemModel { Id = 2, Message = "第二条消息" },
      new ItemModel { Id = 3, Message = "第三条消息" }
    };
    // 预先初始化字典
    foreach (var item in items)
    {
      componentRefs[item.Id] = null;
    }
  }
  private void ShowAllMessages()
  {
    foreach (var component in componentRefs.Values)
    {
      component?.ShowCurrentMessage();
    }
  }
  private void UpdateAllMessages()
  {
    foreach (var item in items)
    {
      if (componentRefs.TryGetValue(item.Id, out var component) && component != null)
      {
        component.UpdateMessage($"更新后的消息 {item.Id}");
      }
    }
  }
  private void HandleMessageChanged((int Id, string Message) data)
  {
    Console.WriteLine($"收到消息更新 - ID: {data.Id}, 消息: {data.Message}");
    var item = items.FirstOrDefault(i => i.Id == data.Id);
    if (item != null)
    {
      item.Message = data.Message;
      StateHasChanged();
    }
  }
  public class ItemModel
  {
    public int Id { get; set; }
    public string Message { get; set; } = string.Empty;
  }
}
<!-- ChildComponent.razor -->
<div class="child-component">
    <h5>子组件 ID: @Id</h5>
    <p>当前消息: <strong>@Message</strong></p>
    <input @bind="currentMessage" @bind:event="oninput" />
    <button @onclick="UpdateMessage" class="btn btn-sm btn-info">更新消息</button>
</div>
@code {
    [Parameter]
    public int Id { get; set; }
    [Parameter]
    public string Message { get; set; } = string.Empty;
    [Parameter]
    public EventCallback<(int Id, string Message)> OnMessageChanged { get; set; }
    private string currentMessage = string.Empty;
    protected override void OnParametersSet()
    {
        currentMessage = Message;
    }
    private async Task UpdateMessage()
    {
        await OnMessageChanged.InvokeAsync((Id, currentMessage));
    }
    public void ShowCurrentMessage()
    {
        Console.WriteLine($"组件 {Id} 的消息: {Message}");
    }
    public void UpdateMessage(string newMessage)
    {
        currentMessage = newMessage;
        UpdateMessage().Wait();
    }
}

4、使用 ref 回调

<CustomInput @ref="SetInputRef" />
@code {
    private CustomInput? inputRef;
    private void SetInputRef(CustomInput component)
    {
        inputRef = component;
        // 组件引用设置后的初始化逻辑
        component?.Initialize();
    }
}

5、与 JavaScript 互操作

@inject IJSRuntime JSRuntime
<div @ref="myDiv" style="width: 100px; height: 100px; background: red;"></div>
<button @onclick="ChangeDivStyle">修改样式</button>
@code {
    private ElementReference myDiv;
    private async Task ChangeDivStyle()
    {
        await JSRuntime.InvokeVoidAsync("changeElementStyle", myDiv);
    }
}

对应的 JavaScript 文件:

// wwwroot/js/site.js
window.changeElementStyle = (element) => {
    element.style.background = 'blue';
    element.style.width = '200px';
};

本章节中用到:IJSRuntime ,后面会详细讲解。

以上就是关于《ASP.NET Core Blazor 核心功能一:Blazor依赖注入与状态管理指南》的全部内容,希望你有所收获。关注、点赞,持续分享

相关文章
|
4月前
|
开发框架 .NET C#
ASP.NET Core Blazor 路由配置和导航
大家好,我是码农刚子。本文系统介绍Blazor单页应用的路由机制,涵盖基础配置、路由参数、编程式导航及高级功能。通过@page指令定义路由,支持参数约束、可选参数与通配符捕获,结合NavigationManager实现页面跳转与参数传递,并演示用户管理、产品展示等典型场景,全面掌握Blazor路由从入门到实战的完整方案。
460 6
|
3月前
|
开发框架 JavaScript 前端开发
ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活
码农刚子带你深入Blazor与JavaScript互操作,掌握IJSRuntime调用、双向通信及集成Chart.js等实战技巧,提升Web开发灵活性。
193 7
ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活
|
4月前
|
开发框架 前端开发 .NET
最新ASP.NET Core Blazor简介和快速入门一(基础篇)
大家好,我是码农刚子。本篇文章介绍了ASP.NET Core Blazor的简介和基础语法。Blazor是微软推出的基于.NET的Web框架,支持C#构建交互式前端,无需JavaScript。提供Server、WebAssembly和Hybrid三种托管模式,分别适用于实时通信、离线运行与跨平台原生应用开发,实现全栈C#开发体验。
340 1
最新ASP.NET Core Blazor简介和快速入门一(基础篇)
|
3月前
|
开发框架 .NET C#
ASP.NET Core Blazor 核心功能二:Blazor表单和验证
本文介绍了Blazor中EditForm组件的使用及三种表单验证方案:基于DataAnnotationsValidator的数据注解验证、自定义ValidationMessageStore实现复杂逻辑验证,以及集成FluentValidation库进行灵活业务规则校验,并提供完整代码示例。
168 6
ASP.NET Core Blazor 核心功能二:Blazor表单和验证
|
缓存 C# 开发者
C# 一分钟浅谈:Blazor Server 端开发
本文介绍了 Blazor Server,一种基于 .NET 的 Web 开发模型,允许使用 C# 和 Razor 语法构建交互式 Web 应用。文章从基础概念、创建应用、常见问题及解决方案、易错点及避免方法等方面详细讲解,帮助开发者快速上手并提高开发效率。
428 2
|
网络协议 算法 Linux
通过实验深入了解 TCP 数据的发送和接收
本系列文章是组内写给新人和实习生的 TCP入门系列教程,结合了理论和实践,本篇为第二篇,建议先读上篇《通过实验深入了解TCP 连接的建立和关闭》。
|
开发框架 前端开发 C#
从零开始学 Blazor 创建 Web 应用,入门指南超详细!带你轻松开启精彩的开发之旅!
【8月更文挑战第31天】在互联网时代,Web应用开发愈发重要,Blazor作为新兴框架,允许使用C#和.NET技术构建交互式Web应用,提高开发效率与代码可维护性。本文将从零开始引导读者了解Blazor的基本概念,安装设置步骤,项目创建及运行方法。通过简单的示例介绍Blazor的基本结构,包括Pages、Shared等文件夹用途,以及Program.cs文件的功能。同时,还将演示如何创建Razor页面和组件,实现数据绑定与事件处理,帮助读者快速入门Blazor开发。
2017 0
|
JavaScript 前端开发 C#
从入门到放弃,我们为何从 Blazor 回到 Vue
【10月更文挑战第29天】在前端开发中,许多开发者从尝试新技术 Blazor 最终回到熟悉的 Vue。主要原因包括:1) Blazor 学习曲线陡峭,Vue 上手容易;2) Vue 开发工具成熟,开发效率高;3) Vue 性能优异,优化简单;4) Vue 社区庞大,生态丰富;5) 项目需求和团队协作更适配 Vue。选择技术栈需综合考虑多方面因素。
1433 0
|
存储 缓存 Unix
victoriaMetrics中的一些Sao操作
victoriaMetrics中的一些Sao操作
192 1
|
JavaScript 前端开发 API
Blazor系统教程
基于.net8的Blazor系统教程
552 6

热门文章

最新文章