潞兴僭谐大家好,我是码农刚子。上一章介绍了Blazor的简介,开发工具及环境,基本语法和一些示例。接下来我们继续了解Blazor 组件相关的基础知识,希望对你有所帮助。

1、组件生命周期

1.简介

Blazor的生命周期与React组件的生命周期类似,也分为三个阶段:初始化阶段、运行中阶段和销毁阶段,其相关方法有10个,包括设置参数前、初始化、设置参数之后、组件渲染后以及组件的销毁,但是这些方法有些是重复的,只不过是同步与异步的区别。

2.图解

首先将结果图呈现,代码位于第3部分:

Blazor生命周期方法主要包括:

1

设置参数前

SetParametersAsync

2

初始化

OnInitialized/OnInitializedAsync

3

设置参数后

OnParametersSet/OnParametersSetAsync

4

组件渲染呈现后

OnAfterRender/OnAfterRenderAsync

5

判断是否渲染组件

ShouldRender

6

组件删除前

Dispose

7

通知组件渲染

StateHasChanged

在所有生命周期函数中,有以下需要注意的点:

(1)前5种方法的声明都是virtual,除SetParametersAsync为public外,其他的都是protected。

(2)OnAfterRender/OnAfterRenderAsync方法有一个bool类型的形参firstRender,用于指示是否是第一次渲染(即组件初始化时的渲染)。

(3)同步方法总是先于异步方法执行。

(4)Dispose函数需要通过使用@implements指令实现IDisposable接口来实现。

(5)StateHasChanged无法被重写,可以被显示调用,以便强制实现组件刷新(如果ShouldRender返回true,并且Blazor认为需要刷新);当组件状态更改时不必显示调用此函数,也可导致组件的重新渲染(如果ShouldRender返回true),因为其已经在ComponentBase内部的处理过程(第一次初始化设置参数时、设置参数后和DOM事件处理等)中被调用。

3.代码示例

设置参数时 (SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。

每次调用 ParameterView 时,方法的 参数都包含该组件的SetParametersAsync值集。 通过重写 SetParametersAsync 方法,C#代码可以直接与 ParameterView 参数交互。

@page "/set-params-async/{Param?}"

Set Parameters Async

Set Parameters Async Example

@message

@code {

private string message = "Not set";

[Parameter]

public string? Param { get; set; }

public override async Task SetParametersAsync(ParameterView parameters)

{

if (parameters.TryGetValue(nameof(Param), out var value))

{

if (value is null)

{

message = "The value of 'Param' is null.";

}

else

{

message = $"The value of 'Param' is {value}.";

}

}

await base.SetParametersAsync(parameters);

}

}

组件初始化 (OnInitialized 和 OnInitializedAsync 专门用于在组件实例的整个生命周期内初始化组件。 参数值和参数值更改不应影响在这些方法中执行的初始化。 例如,将静态选项加载到下拉列表中,该下拉列表在组件的生命周期内不会更改,也不依赖于参数值,这是在这些生命周期方法之一中执行的操作。 如果参数值或参数值更改会影响组件状态,请改为使用 OnParametersSet{Async}。

组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用这些方法。

如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

对于同步操作,重写 OnInitialized:

@page "/on-init"

On Initialized

On Initialized Example

@message

@code {

private string? message;

protected override void OnInitialized() =>

message = $"Initialized at {DateTime.Now}";

}

若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:

protected override async Task OnInitializedAsync()

{

//await ...

await Task.Delay(2000); //2秒之后

message = $"Initialized at {DateTime.Now} after 2 second delay";

}

如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnInitializedAsync:

protected override async Task OnInitializedAsync()

{

await ...

await base.OnInitializedAsync();

}

设置参数之后 (OnParametersSet 或 OnParametersSetAsync 在以下情况下调用:

在 OnInitialized 或 OnInitializedAsync 中初始化组件后。

当父组件重新呈现并提供以下内容时:

至少一个参数已更改时的已知或基元不可变类型。

复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

在组件路由中,不能同时对DateTime参数使用datetime路由约束,并将该参数设为可选。 因此,以下 OnParamsSet 组件使用两个 @page 指令来处理具有和没有 URL 中提供的日期段的路由。

@page "/on-params-set"

@page "/on-params-set/{StartDate:datetime}"

On Parameters Set

On Parameters Set Example

Pass a datetime in the URI of the browser's address bar.

For example, add /1-1-2024 to the address.

@message

@code {

private string? message;

[Parameter]

public DateTime StartDate { get; set; }

protected override void OnParametersSet()

{

if (StartDate == default)

{

StartDate = DateTime.Now;

message = $"No start date in URL. Default value applied " +

$"(StartDate: {StartDate}).";

}

else

{

message = $"The start date in the URL was used " +

$"(StartDate: {StartDate}).";

}

}

}

应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:

protected override async Task OnParametersSetAsync()

{

await ...

}

如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnParametersSetAsync:

protected override async Task OnParametersSetAsync()

{

await ...

await base.OnParametersSetAsync();

}

组件呈现之后 (OnAfterRender 和 OnAfterRenderAsync 在组件以交互方式呈现并且 UI 完成更新之后被调用(例如,元素添加到浏览器 DOM 之后)。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。

这些方法不会在预呈现或静态服务器端渲染(静态 SSR)期间在服务器上调用,因为这些进程未附加到实时浏览器 DOM,并且已在 DOM 更新之前完成。

对于 OnAfterRenderAsync,组件在任何返回 Task 的操作完成后不会自动重渲染,以避免无限渲染循环。

firstRender 和 OnAfterRender 的 OnAfterRenderAsync 参数:

在第一次呈现组件实例时设置为 true。

可用于确保初始化操作仅执行一次。

@page "/after-render"

@inject ILogger Logger

After Render

After Render Example

Study logged messages in the console.

@code {

protected override void OnAfterRender(bool firstRender) =>

Logger.LogInformation("firstRender = {FirstRender}", firstRender);

private void HandleClick() => Logger.LogInformation("HandleClick called");

}

加载页面并选择按钮时,AfterRender.razor 示例向控制台输出以下内容:

在渲染后立即进行的异步工作必须在 OnAfterRenderAsync 生命周期事件期间发生:

protected override async Task OnAfterRenderAsync(bool firstRender)

{

...

}

如果自定义基类与自定义初始化逻辑一起使用,需在基类上调用 OnAfterRenderAsync:

protected override async Task OnAfterRenderAsync(bool firstRender)

{

...

await base.OnAfterRenderAsync(firstRender);

}

基类生命周期方法

重写 Blazor 的生命周期方法时,无需为 ComponentBase 调用基类生命周期方法。 但在以下情况下,组件应调用重写的基类生命周期方法:

重写 ComponentBase.SetParametersAsync 时,通常会调用 await base.SetParametersAsync(parameters);, 因为基类方法会调用其他生命周期方法并以复杂的方式触发渲染。 有关详细信息,请参阅设置参数时 (SetParametersAsync) 部分。

如果基类方法包含必须执行的逻辑。 库使用者通常在继承基类时调用基类生命周期方法,因为库基类通常具有要执行的自定义生命周期逻辑。 如果应用使用某个库中的基类,请参阅该库的文档以获取指导。

以下示例中调用了 base.OnInitialized(); 以确保会执行基类的 OnInitialized 方法。 如果没有调用,BlazorRocksBase2.OnInitialized 不会执行。

@page "/blazor-rocks-2"

@inherits BlazorRocksBase2

@inject ILogger Logger

Blazor Rocks!

Blazor Rocks! Example 2

@BlazorRocksText

@code {

protected override void OnInitialized()

{

Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

base.OnInitialized();

}

}

using Microsoft.AspNetCore.Components;

namespace BlazorAppWasm

{

public class BlazorRocksBase2: ComponentBase

{

[Inject]

private ILogger Logger { get; set; } = default!;

public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";

protected override void OnInitialized() =>

Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");

}

}

2、数据绑定

Blazor提供了强大的数据绑定机制,主要包括单向绑定和双向绑定两种模式。

1. 单向数据绑定

单向绑定是指数据从组件流向UI,但UI的变化不会自动更新数据源。

基本语法

当前值: @currentValue

用户名: @UserName

创建时间: @CreateTime.ToString("yyyy-MM-dd")

完整示例

单向绑定示例

计数器: @count

消息: @message

用户信息: @user.Name - @user.Age

@code {

private int count = 0;

private string message = "初始消息";

private User user = new User { Name = "张三", Age = 25 };

private void Increment()

{

count++;

// StateHasChanged(); // 通常不需要手动调用,事件处理会自动触发重新渲染

}

private void ChangeMessage()

{

message = $"消息已更新: {DateTime.Now:HH:mm:ss}";

}

private void UpdateUser()

{

user = new User { Name = "李四", Age = 30 };

}

class User

{

public string Name { get; set; } = string.Empty;

public int Age { get; set; }

}

}

2. 双向数据绑定

双向绑定允许数据在组件和UI之间双向流动:UI变化自动更新数据源,数据源变化自动更新UI。

基本语法

  • ...
  • 完整示例

    双向绑定示例

    显示: @userName

    显示: @email

    显示: @age

    选择: @selectedCity

    @(isAgreed ? "已同意" : "未同意")

    汇总信息:

    用户名: @userName

    邮箱: @email

    年龄: @age

    城市: @selectedCity

    同意协议: @isAgreed

    @code {

    private string userName = string.Empty;

    private string email = string.Empty;

    private int age = 0;

    private string selectedCity = string.Empty;

    private bool isAgreed = false;

    }

    3. 绑定事件控制

    3.1 绑定特定事件

    默认情况下,@bind 在失去焦点时更新。可以使用 @bind:event 指定触发事件:

    实时绑定示例

    placeholder="输入搜索内容..." />

    实时搜索: @searchText

    默认绑定: @normalText

    @code {

    private string searchText = string.Empty;

    private string normalText = string.Empty;

    }

    3.2 绑定格式化

    格式化绑定示例

    选择的日期: @startDate.ToString("yyyy年MM月dd日")

    价格: @price.ToString("C")

    @code {

    private DateTime startDate = DateTime.Today;

    private decimal price = 0.00m;

    }

    4. 自定义组件双向绑定

    在自定义组件中实现双向绑定:

    子组件

    value="@Value"

    @οninput="HandleInput"

    class="form-control @AdditionalClass"

    placeholder="@Placeholder" />

    @if (!string.IsNullOrEmpty(ValidationMessage))

    {

    @ValidationMessage

    }

    @code {

    [Parameter]

    public string Value { get; set; } = string.Empty;

    [Parameter]

    public EventCallback ValueChanged { get; set; }

    [Parameter]

    public string Label { get; set; } = string.Empty;

    [Parameter]

    public string Placeholder { get; set; } = string.Empty;

    [Parameter]

    public string AdditionalClass { get; set; } = string.Empty;

    [Parameter]

    public string ValidationMessage { get; set; } = string.Empty;

    private async Task HandleInput(ChangeEventArgs e)

    {

    Value = e.Value?.ToString() ?? string.Empty;

    await ValueChanged.InvokeAsync(Value);

    }

    }

    父组件使用

    自定义组件双向绑定

    @bind-Value="userName"

    Label="用户名"

    Placeholder="请输入用户名" />

    @bind-Value="email"

    Label="邮箱"

    Placeholder="请输入邮箱地址"

    ValidationMessage="@(IsValidEmail ? "" : "邮箱格式不正确")" />

    用户名: @userName

    邮箱: @email

    @code {

    private string userName = string.Empty;

    private string email = string.Empty;

    private bool IsValidEmail => email.Contains("@") && email.Contains(".");

    }

    5.复杂对象绑定

    复杂对象绑定

    用户信息

    当前用户信息:

    @userInfoJson

    @code {

    private User currentUser = new User();

    private string userInfoJson =>

    System.Text.Json.JsonSerializer.Serialize(currentUser, new System.Text.Json.JsonSerializerOptions

    {

    WriteIndented = true

    });

    private void ResetUser()

    {

    currentUser = new User();

    }

    private void CreateNewUser()

    {

    currentUser = new User

    {

    Name = "新用户",

    Age = 18,

    Address = new Address { Street = "新建街道", City = "新建城市" }

    };

    }

    class User

    {

    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }

    public Address Address { get; set; } = new Address();

    }

    class Address

    {

    public string Street { get; set; } = string.Empty;

    public string City { get; set; } = string.Empty;

    }

    }

    6.绑定模式对比

    绑定类型

    语法

    更新时机

    适用场景

    单向绑定

    @property

    数据源变化时

    显示数据、计算属性

    双向绑定

    @bind="property"

    失去焦点时

    表单输入、用户交互

    实时双向

    @bind="property" @bind:event="oninput"

    输入时实时更新

    搜索框、实时验证

    自定义绑定

    @bind-Value="property"

    自定义事件触发

    自定义表单组件

    3、事件处理

    1. 基本事件处理

    1.1 单击事件

    单击事件示例

    最后点击的按钮: @lastClickedButton

    点击次数: @clickCount

    @code {

    private int lastClickedButton = 0;

    private int clickCount = 0;

    private void HandleClick()

    {

    clickCount++;

    Console.WriteLine("按钮被点击了!");

    }

    private void HandleButtonClick(int buttonNumber)

    {

    lastClickedButton = buttonNumber;

    clickCount++;

    StateHasChanged();

    }

    }

    1.2 异步事件处理

    异步事件处理

    操作结果: @operationResult

    耗时: @elapsedTime 毫秒

    @code {

    private bool isLoading = false;

    private string operationResult = string.Empty;

    private long elapsedTime = 0;

    private async Task HandleAsyncClick()

    {

    isLoading = true;

    operationResult = "操作开始...";

    var stopwatch = System.Diagnostics.Stopwatch.StartNew();

    // 模拟异步操作

    await Task.Delay(2000);

    stopwatch.Stop();

    elapsedTime = stopwatch.ElapsedMilliseconds;

    operationResult = $"操作完成!数据已保存。";

    isLoading = false;

    StateHasChanged();

    }

    }

    2. 表单事件处理

    2.1 输入事件

    表单事件处理

    @οnchange="HandleChange"

    class="form-control"

    placeholder="输入内容..." />

    实时输入: @inputValue | 变化事件: @changeValue

    选择的值: @selectedValue

    同意条款

    状态: @(isChecked ? "已选中" : "未选中")

    表单数据:

    @System.Text.Json.JsonSerializer.Serialize(user, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })

    提交状态: @submitStatus

    @code {

    private string inputValue = string.Empty;

    private string changeValue = string.Empty;

    private string selectedValue = string.Empty;

    private bool isChecked = false;

    private string submitStatus = "未提交";

    private User user = new User();

    private void HandleInput(ChangeEventArgs e)

    {

    inputValue = e.Value?.ToString() ?? string.Empty;

    }

    private void HandleChange(ChangeEventArgs e)

    {

    changeValue = e.Value?.ToString() ?? string.Empty;

    }

    private void HandleSelectChange(ChangeEventArgs e)

    {

    selectedValue = e.Value?.ToString() ?? string.Empty;

    }

    private void HandleCheckboxChange(ChangeEventArgs e)

    {

    isChecked = (bool)(e.Value ?? false);

    }

    private void HandleSubmit()

    {

    submitStatus = "表单提交(可能有验证错误)";

    }

    private void HandleValidSubmit()

    {

    submitStatus = $"表单验证通过!数据已保存 - {DateTime.Now:HH:mm:ss}";

    // 这里可以调用API保存数据

    }

    class User

    {

    public string Username { get; set; } = string.Empty;

    public string Email { get; set; } = string.Empty;

    }

    }

    3. 鼠标和键盘事件

    3.1 鼠标事件

    鼠标事件

    @οnmοusedοwn="HandleMouseDown"

    @οnmοuseup="HandleMouseUp"

    @οnmοusemοve="HandleMouseMove"

    @οnmοuseοver="HandleMouseOver"

    @οnmοuseοut="HandleMouseOut"

    @οnclick="HandleAreaClick"

    @οndblclick="HandleDoubleClick"

    style="width: 300px; height: 200px; border: 2px solid #007bff; padding: 20px; margin: 10px 0;">

    鼠标交互区域

    事件日志:

    • @foreach (var log in eventLogs.TakeLast(10).Reverse())

      {

      • @log

      }

    鼠标位置: (@mouseX, @mouseY)

    按钮状态: @(isMouseDown ? "按下" : "释放")

    悬停状态: @(isMouseOver ? "在区域内" : "在区域外")

    @code {

    private double mouseX = 0;

    private double mouseY = 0;

    private bool isMouseDown = false;

    private bool isMouseOver = false;

    private List eventLogs = new List();

    private void LogEvent(string eventName)

    {

    eventLogs.Add($"{DateTime.Now:HH:mm:ss.fff} - {eventName}");

    StateHasChanged();

    }

    private void HandleMouseDown(MouseEventArgs e)

    {

    isMouseDown = true;

    LogEvent($"MouseDown - 按钮: {e.Button}, 位置: ({e.ClientX}, {e.ClientY})");

    }

    private void HandleMouseUp(MouseEventArgs e)

    {

    isMouseDown = false;

    LogEvent($"MouseUp - 按钮: {e.Button}, 位置: ({e.ClientX}, {e.ClientY})");

    }

    private void HandleMouseMove(MouseEventArgs e)

    {

    mouseX = e.ClientX;

    mouseY = e.ClientY;

    // 注意:频繁触发,生产环境需要节流

    // LogEvent($"MouseMove - 位置: ({e.ClientX}, {e.ClientY})");

    }

    private void HandleMouseOver(MouseEventArgs e)

    {

    isMouseOver = true;

    LogEvent("MouseOver");

    }

    private void HandleMouseOut(MouseEventArgs e)

    {

    isMouseOver = false;

    LogEvent("MouseOut");

    }

    private void HandleAreaClick(MouseEventArgs e)

    {

    LogEvent($"Click - 按钮: {e.Button}");

    }

    private void HandleDoubleClick(MouseEventArgs e)

    {

    LogEvent($"DoubleClick - 按钮: {e.Button}");

    }

    }

    3.2 键盘事件

    键盘事件

    @οnkeyup="HandleKeyUp"

    @οnkeypress="HandleKeyPress"

    class="form-control"

    placeholder="在这里输入并观察键盘事件..." />

    键盘事件日志:

    • @foreach (var log in keyEventLogs.TakeLast(10).Reverse())

      {

      • @log

      }

    最后按下的键: @lastKey

    Ctrl 按下: @(isCtrlPressed ? "是" : "否")

    Shift 按下: @(isShiftPressed ? "是" : "否")

    Alt 按下: @(isAltPressed ? "是" : "否")

    @code {

    private string lastKey = "无";

    private bool isCtrlPressed = false;

    private bool isShiftPressed = false;

    private bool isAltPressed = false;

    private List keyEventLogs = new List();

    private void LogKeyEvent(string eventName, KeyboardEventArgs e)

    {

    var log = $"{DateTime.Now:HH:mm:ss.fff} - {eventName}: Key='{e.Key}', Code='{e.Code}'";

    if (e.CtrlKey) log += " [Ctrl]";

    if (e.ShiftKey) log += " [Shift]";

    if (e.AltKey) log += " [Alt]";

    keyEventLogs.Add(log);

    StateHasChanged();

    }

    private void HandleKeyDown(KeyboardEventArgs e)

    {

    lastKey = e.Key;

    isCtrlPressed = e.CtrlKey;

    isShiftPressed = e.ShiftKey;

    isAltPressed = e.AltKey;

    LogKeyEvent("KeyDown", e);

    // 快捷键处理示例

    if (e.CtrlKey && e.Key == "s")

    {

    e.PreventDefault(); // 阻止浏览器默认保存行为

    LogKeyEvent("快捷键: Ctrl+S", e);

    }

    }

    private void HandleKeyUp(KeyboardEventArgs e)

    {

    isCtrlPressed = e.CtrlKey;

    isShiftPressed = e.ShiftKey;

    isAltPressed = e.AltKey;

    LogKeyEvent("KeyUp", e);

    }

    private void HandleKeyPress(KeyboardEventArgs e)

    {

    LogKeyEvent("KeyPress", e);

    }

    }

    4. 焦点和剪贴板事件

    焦点和剪贴板事件

    @οnblur="HandleBlur"

    class="form-control"

    placeholder="点击获取焦点,点击别处失去焦点" />

    事件状态:

    焦点状态: @(hasFocus ? "有焦点" : "无焦点")

    最后操作: @lastOperation

    剪贴板内容: @clipboardContent

    @code {

    private bool hasFocus = false;

    private string lastOperation = "无";

    private string clipboardContent = "无";

    private void HandleFocus(FocusEventArgs e)

    {

    hasFocus = true;

    lastOperation = "获得焦点";

    StateHasChanged();

    }

    private void HandleBlur(FocusEventArgs e)

    {

    hasFocus = false;

    lastOperation = "失去焦点";

    StateHasChanged();

    }

    private void HandleCopy(ClipboardEventArgs e)

    {

    lastOperation = "复制操作";

    clipboardContent = "复制的内容无法直接获取(安全限制)";

    StateHasChanged();

    }

    private void HandleCut(ClipboardEventArgs e)

    {

    lastOperation = "剪切操作";

    clipboardContent = "剪切的内容无法直接获取(安全限制)";

    StateHasChanged();

    }

    private void HandlePaste(ClipboardEventArgs e)

    {

    lastOperation = "粘贴操作";

    clipboardContent = "粘贴的内容无法直接获取(安全限制)";

    StateHasChanged();

    }

    }

    5. 自定义事件处理

    5.1 事件参数封装

    自定义事件处理

    父级区域(点击会触发)

    自定义操作:

    操作日志:

    • @foreach (var log in actionLogs.TakeLast(5).Reverse())

      {

      • @log

      }

    @code {

    private List actionLogs = new List();

    private void LogAction(string action)

    {

    actionLogs.Add($"{DateTime.Now:HH:mm:ss} - {action}");

    StateHasChanged();

    }

    private void HandleParentClick()

    {

    LogAction("父级区域被点击");

    }

    private void HandleChildClick()

    {

    LogAction("子按钮被点击(事件不会冒泡)");

    }

    private void HandleChildClickWithPrevent()

    {

    LogAction("阻止默认行为的按钮被点击");

    }

    private void HandleCustomAction1()

    {

    LogAction("执行自定义操作1");

    // 自定义业务逻辑

    }

    private void HandleCustomAction2(MouseEventArgs e)

    {

    LogAction($"执行自定义操作2 - 点击位置: ({e.ClientX}, {e.ClientY})");

    // 自定义业务逻辑

    }

    private async Task HandleCustomAsyncAction()

    {

    LogAction("开始异步操作");

    await Task.Delay(1000);

    LogAction("异步操作完成");

    }

    }

    6. 事件处理最佳实践

    6.1 性能优化

    事件处理性能优化

    @foreach (var item in items)

    {

    @item.Name

    }

    @foreach (var item in largeList)

    {

    @item.Name

    }

    操作日志:

    • @foreach (var log in actionLogs.TakeLast(5).Reverse())

      {

      • @log

      }

    @code {

    private List items = new List

    {

    new Item { Id = 1, Name = "项目1" },

    new Item { Id = 2, Name = "项目2" },

    new Item { Id = 3, Name = "项目3" }

    };

    private List largeList = Enumerable.Range(1, 100)

    .Select(i => new Item { Id = i, Name = $"项目{i}" })

    .ToList();

    private List actionLogs = new List();

    private void DeleteItem(int id)

    {

    items.RemoveAll(i => i.Id == id);

    LogAction($"删除了项目 {id}");

    }

    private void HandleListItemClick(MouseEventArgs e, int itemId)

    {

    // 通过参数 itemId 就知道是哪个按钮被点击了

    Console.WriteLine($"Clicked item ID: {itemId}");

    }

    // 添加 LogAction 方法

    private void LogAction(string action)

    {

    actionLogs.Add($"{DateTime.Now:HH:mm:ss} - {action}");

    StateHasChanged();

    }

    class Item

    {

    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    }

    }

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐