Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/BootstrapBlazor/Components/Table/Table.razor
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,15 @@
{
if (item is IDynamicObject d)
{
<Checkbox Value="@d.DynamicObjectPrimaryKey" State="@RowCheckState(item)" ShowLabel="false"
<Checkbox Value="@d.DynamicObjectPrimaryKey"
State="@RowCheckState(item)" ShowLabel="false"
OnStateChanged="OnCheckGuid" StopPropagation="true">
</Checkbox>
}
else
{
<Checkbox Value="@item" State="@RowCheckState(item)" ShowLabel="false"
<Checkbox Value="@item"
State="@RowCheckState(item)" ShowLabel="false"
OnStateChanged="OnCheck" StopPropagation="true">
</Checkbox>
}
Expand Down
121 changes: 121 additions & 0 deletions test/UnitTest/Components/TableTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Reflection;
Expand Down Expand Up @@ -6474,6 +6475,126 @@ public async Task DynamicContext_EqualityComparer()
Assert.True(compared);
}

[Fact]
public async Task DynamicContext_CardView_CheckGuid_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var selectedRows = new List<DynamicObject>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<DynamicObject>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.CardView);
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.DynamicContext, CreateDynamicContext(localizer));
pb.Add(a => a.SelectedRows, selectedRows);
pb.Add(a => a.SelectedRowsChanged, EventCallback.Factory.Create<List<DynamicObject>>(this, rows => selectedRows = rows));
});
});

// CardView 模式无表头
// 共 2 行数据选中第一行数据
var input = cut.FindComponents<Checkbox<Guid>>()[0];
await cut.InvokeAsync(input.Instance.OnToggleClick);

Assert.Single(selectedRows);
Assert.Equal(0, selectedRows[0].GetValue("Id"));

// 取消选中
await cut.InvokeAsync(input.Instance.OnToggleClick);
Assert.Empty(selectedRows);
}

[Fact]
public async Task DynamicContext_HeaderCheckGuid_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var selectedRows = new List<DynamicObject>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<DynamicObject>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.DynamicContext, CreateDynamicContext(localizer));
pb.Add(a => a.SelectedRows, selectedRows);
pb.Add(a => a.SelectedRowsChanged, EventCallback.Factory.Create<List<DynamicObject>>(this, rows => selectedRows = rows));
});
});

// 无选中行
Assert.Empty(selectedRows);

// 点击表头全选,选中行为 2 行
var header = cut.FindComponents<Checkbox<Guid>>()[0];
await cut.InvokeAsync(header.Instance.OnToggleClick);
Assert.Equal(2, selectedRows.Count);

// 再次点击表头全选,取消全选
await cut.InvokeAsync(header.Instance.OnToggleClick);
Assert.Empty(selectedRows);
}

[Fact]
public async Task DynamicContext_HeaderCheckGuid_ShowRowCheckboxCallback_Ok()
{
// 测试包含无法选中行逻辑
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var selectedRows = new List<DynamicObject>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<DynamicObject>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.DynamicContext, CreateDynamicContext(localizer));
pb.Add(a => a.ShowRowCheckboxCallback, row => Equals(row.GetValue("Id"), 0));
pb.Add(a => a.SelectedRows, selectedRows);
pb.Add(a => a.SelectedRowsChanged, EventCallback.Factory.Create<List<DynamicObject>>(this, rows => selectedRows = rows));
});
});

Assert.Equal(2, cut.FindComponents<Checkbox<Guid>>().Count);

var header = cut.FindComponents<Checkbox<Guid>>()[0];
await cut.InvokeAsync(header.Instance.OnToggleClick);
Assert.Single(selectedRows);
}


[Fact]
public async Task DynamicContext_ChangeDetection_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<DynamicObject>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.DynamicContext, CreateDynamicContext(localizer));
});
});

cut.Dispose();

// 表格使用动态创建类型后,不能被 Blazor 底层 ChangeDetection 缓存,否则生成的动态 Assembly 无法被释放
// 通过反射查看是否被缓存
var type = typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.ChangeDetection");
Assert.NotNull(type);

var fieldInfo = type.GetField("_immutableObjectTypesCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
Assert.NotNull(fieldInfo);

IEnumerable<Type>? items = null;
if (fieldInfo.GetValue(null) is ConcurrentDictionary<Type, bool> cache)
{
items = cache.Keys.Where(i => i.Assembly.GetName().Name == "BootstrapBlazor_DynamicAssembly");
}
Assert.NotNull(items);
Assert.Empty(items);
Comment on lines +6583 to +6595
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test relies on Blazor internal implementation details (internal type name Microsoft.AspNetCore.Components.ChangeDetection and private static field _immutableObjectTypesCache, plus a concrete ConcurrentDictionary<Type, bool> shape). Because CI uses a floating SDK (10.0.x), a framework patch could rename/move the type/field or change the cache type and cause unrelated test failures. Consider rewriting the assertion to validate unload without depending on private runtime fields (e.g., keep a WeakReference to a dynamically emitted type/assembly, dispose the component, force GC, and assert it can be collected), or at least tolerate missing/changed members by conditionally skipping/failing with a clearer message.

Copilot uses AI. Check for mistakes.
}

[Fact]
public async Task DynamicContext_Add()
{
Expand Down
Loading