Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ private void CreateContext()
DataTableDynamicContext = new DataTableDynamicContext(UserData, (context, col) =>
{
var propertyName = col.GetFieldName();
// 使用 Text 设置显示名称示例
col.Text = FooLocalizer[propertyName];
if (propertyName == nameof(Foo.DateTime))
{
context.AddRequiredAttribute(nameof(Foo.DateTime));
Expand All @@ -56,8 +58,6 @@ private void CreateContext()
else if (propertyName == nameof(Foo.Name))
{
context.AddRequiredAttribute(nameof(Foo.Name), FooLocalizer["Name.Required"]);
// 使用 Text 设置显示名称示例
col.Text = FooLocalizer[nameof(Foo.Name)];
}
else if (propertyName == nameof(Foo.Count))
{
Expand Down
31 changes: 22 additions & 9 deletions src/BootstrapBlazor/Dynamic/DataTableDynamicContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class DataTableDynamicContext : DynamicObjectContext
/// <para lang="zh">负责将 DataRow 与 Items 关联起来方便查找提高效率</para>
/// <para lang="en">Responsible for associating DataRow with Items to facilitate lookup and improve efficiency</para>
/// </summary>
private ConcurrentDictionary<Guid, (IDynamicObject DynamicObject, DataRow Row)> Caches { get; } = new();
private ConcurrentDictionary<Guid, (IDynamicObject DynamicObject, DataRow Row)> _dataCache = new();
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

/// <summary>
/// <para lang="zh">添加行回调委托</para>
Expand Down Expand Up @@ -90,7 +90,20 @@ public DataTableDynamicContext(DataTable table, Action<DataTableDynamicContext,
[ExcludeFromCodeCoverage]
Type CreateType()
{
var dynamicType = EmitHelper.CreateTypeByName($"BootstrapBlazor_{nameof(DataTableDynamicContext)}_{GetHashCode()}", cols, typeof(DataTableDynamicObject), OnColumnCreating);
// Emit 生成动态类 (使用缓存)
var columnNames = string.Join('|', table.Columns.Cast<DataColumn>().Select(static c => $"{c.ColumnName}:{c.DataType.FullName}"));
var cacheKey = $"BootstrapBlazor-{nameof(DataTableDynamicContext)}-{columnNames}";
Comment thread
ArgoZhang marked this conversation as resolved.
var dynamicType = CacheManager.GetDynamicObjectTypeByName(cacheKey, cols, OnColumnCreating, out var cached);

Comment on lines +93 to +97
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The dynamic type cache key only includes the DataTable schema (column names/types), but the emitted type also depends on AddAttributesCallback/OnColumnCreating (e.g., required/error messages, display attributes, ignore settings). If two contexts share the same schema but provide different callbacks/localizers, the second context will reuse the first type and its baked-in attributes, causing incorrect column metadata/validation (this is already reproducible in TablesDynamic.razor.cs, where two contexts use different localizers with the same schema). Consider incorporating callback identity and/or CultureInfo.CurrentUICulture into the cache key, or disabling type caching when attribute generation is callback-dependent.

Copilot uses AI. Check for mistakes.
// 缓存命中时仍需调用回调以处理列属性
if (cached && AddAttributesCallback != null)
{
foreach (var col in cols)
{
AddAttributesCallback?.Invoke(this, col);
}
}

Comment thread
ArgoZhang marked this conversation as resolved.
return dynamicType ?? throw new InvalidOperationException();
}
}
Expand Down Expand Up @@ -136,7 +149,7 @@ public override IEnumerable<IDynamicObject> GetItems()

private List<IDynamicObject> BuildItems()
{
Caches.Clear();
_dataCache.Clear();
var ret = new List<IDynamicObject>();
foreach (DataRow row in DataTable.Rows)
{
Expand All @@ -155,7 +168,7 @@ private List<IDynamicObject> BuildItems()

d.Row = row;
d.DynamicObjectPrimaryKey = Guid.NewGuid();
Caches.TryAdd(d.DynamicObjectPrimaryKey, (d, row));
_dataCache.TryAdd(d.DynamicObjectPrimaryKey, (d, row));
ret.Add(d);
}
}
Expand Down Expand Up @@ -203,7 +216,7 @@ public override async Task AddAsync(IEnumerable<IDynamicObject> selectedItems)
var indexOfRow = 0;
var item = selectedItems.FirstOrDefault();

if (item != null && Caches.TryGetValue(item.DynamicObjectPrimaryKey, out var c))
if (item != null && _dataCache.TryGetValue(item.DynamicObjectPrimaryKey, out var c))
{
indexOfRow = DataTable.Rows.IndexOf(c.Row);
}
Expand Down Expand Up @@ -232,7 +245,7 @@ public override async Task AddAsync(IEnumerable<IDynamicObject> selectedItems)
Items?.Insert(indexOfRow, dynamicObject);

// 缓存更新数据
Caches.TryAdd(dynamicObject.DynamicObjectPrimaryKey, (dynamicObject, row));
_dataCache.TryAdd(dynamicObject.DynamicObjectPrimaryKey, (dynamicObject, row));
}
}

Expand All @@ -252,15 +265,15 @@ public override async Task<bool> DeleteAsync(IEnumerable<IDynamicObject> items)
var changed = false;
foreach (var item in items)
{
if (Caches.TryGetValue(item.DynamicObjectPrimaryKey, out var row))
if (_dataCache.TryGetValue(item.DynamicObjectPrimaryKey, out var row))
{
changed = true;

// 删除数据源
DataTable.Rows.Remove(row.Row);

// 清理缓存
Caches.TryRemove(item.DynamicObjectPrimaryKey, out _);
_dataCache.TryRemove(item.DynamicObjectPrimaryKey, out _);

// 清理 Table 组件数据源
Items?.Remove(item);
Expand Down Expand Up @@ -289,7 +302,7 @@ public override async Task<bool> DeleteAsync(IEnumerable<IDynamicObject> items)
private Task OnCellValueChanged(IDynamicObject item, ITableColumn column, object? val)
{
// 更新内部 DataRow
if (Caches.TryGetValue(item.DynamicObjectPrimaryKey, out var cacheItem))
if (_dataCache.TryGetValue(item.DynamicObjectPrimaryKey, out var cacheItem))
{
cacheItem.Row[column.GetFieldName()] = val;
Items = null;
Expand Down
15 changes: 15 additions & 0 deletions src/BootstrapBlazor/Services/CacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

#if NET8_0_OR_GREATER
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -764,4 +765,18 @@ public static object GetFormatterInvoker(Type type, Func<object, Task<string?>>

private static Func<TType, Task<string?>> InvokeFormatterAsync<TType>(Func<object?, Task<string?>> formatter) => new(v => formatter(v));
#endregion

internal static Type? GetDynamicObjectTypeByName(string key, IEnumerable<ITableColumn> cols, Func<ITableColumn, IEnumerable<CustomAttributeBuilder>>? creatingCallback, out bool cached)
{
var created = false;
var type = Instance.GetOrCreate(key, _ =>
{
created = true;
return EmitHelper.CreateTypeByName($"BootstrapBlazor_{nameof(DataTableDynamicContext)}_{key}", cols, typeof(DataTableDynamicObject), creatingCallback);
});
Comment thread
ArgoZhang marked this conversation as resolved.
Comment on lines +773 to +776
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The emitted type name is built from key ($"BootstrapBlazor_{nameof(DataTableDynamicContext)}_{key}"), but the cache key can become very long and may contain characters from column names (e.g., |, :, spaces) and generic type full names. This can bloat metadata and risks DefineType failures for large schemas. Consider generating a short/sanitized type name (e.g., a stable hash of the schema + any other discriminators) while keeping the full string only as the cache key.

Copilot uses AI. Check for mistakes.

// 是否从缓存中获取到的值
cached = !created;
return type;
}
}
Loading