diff --git a/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor b/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor index 48f3aabc969..adeda25536b 100644 --- a/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor +++ b/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor @@ -1,13 +1,16 @@ -
+

@Title

- +
- - + + + + +
diff --git a/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor.cs b/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor.cs index 9d5f1ab4b8c..3f4b848654e 100644 --- a/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Components/AttributeTable.razor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone @@ -6,7 +6,7 @@ namespace BootstrapBlazor.Server.Components.Components; /// -/// +/// 组件参数表格组件 /// public sealed partial class AttributeTable { @@ -15,24 +15,42 @@ public sealed partial class AttributeTable private IStringLocalizer? Localizer { get; set; } /// - /// + /// 获得/设置 表格标题 /// [Parameter] [NotNull] public string? Title { get; set; } /// - /// + /// 获得/设置 表格数据 /// - [Parameter] public IEnumerable? Items { get; set; } + [Parameter] + public IEnumerable Items { get; set; } = []; + + /// + /// 获得/设置 表格关联组件类型 + /// + [Parameter] + public Type? Type { get; set; } /// - /// OnInitialized 方法 + /// 是否显示合计信息 默认 false + /// + [Parameter] + public bool ShowFooter { get; set; } + + /// + /// /// protected override void OnInitialized() { base.OnInitialized(); Title ??= Localizer[nameof(Title)]; + + if (Type != null) + { + Items = ComponentAttributeCacheService.GetAttributes(Type); + } } } diff --git a/src/BootstrapBlazor.Server/Components/Samples/Circles.razor b/src/BootstrapBlazor.Server/Components/Samples/Circles.razor index cf5740f3201..89f2fa54a1a 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Circles.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Circles.razor @@ -12,15 +12,17 @@ -
- - +
+
+ + +
@@ -53,4 +55,4 @@ - + diff --git a/src/BootstrapBlazor.Server/Components/Samples/Circles.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Circles.razor.cs index 897ed8eff9e..c0c58581cd3 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Circles.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Circles.razor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone @@ -17,61 +17,5 @@ private void Add(int interval) _circleValue += interval; _circleValue = Math.Min(100, Math.Max(0, _circleValue)); } - - /// - /// GetAttributes - /// - /// - private AttributeItem[] GetAttributes() => - [ - new() - { - Name = "Width", - Description = Localizer["Width"], - Type = "int", - ValueList = "", - DefaultValue = "120" - }, - new() - { - Name = "StrokeWidth", - Description = Localizer["StrokeWidth"], - Type = "int", - ValueList = "", - DefaultValue = "2" - }, - new() - { - Name = "Value", - Description = Localizer["Value"], - Type = "int", - ValueList = "0-100", - DefaultValue = "0" - }, - new() - { - Name = "Color", - Description = Localizer["Color"], - Type = "Color", - ValueList = "Primary / Secondary / Success / Danger / Warning / Info / Dark", - DefaultValue = "Primary" - }, - new() - { - Name = "ShowProgress", - Description = Localizer["ShowProgress"], - Type = "bool", - ValueList = "true / false", - DefaultValue = "true" - }, - new() - { - Name = "ChildContent", - Description = Localizer["ChildContent"], - Type = "RenderFragment", - ValueList = "", - DefaultValue = "" - } - ]; } diff --git a/src/BootstrapBlazor.Server/Data/AttributeItem.cs b/src/BootstrapBlazor.Server/Data/AttributeItem.cs index fe24314716a..e761968fd09 100644 --- a/src/BootstrapBlazor.Server/Data/AttributeItem.cs +++ b/src/BootstrapBlazor.Server/Data/AttributeItem.cs @@ -1,10 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using System.ComponentModel; - namespace BootstrapBlazor.Server.Data; /// @@ -15,30 +13,30 @@ public class AttributeItem /// /// 获得/设置 参数 /// - [DisplayName("参数")] public string Name { get; set; } = ""; /// /// 获得/设置 说明 /// - [DisplayName("说明")] public string Description { get; set; } = ""; /// /// 获得/设置 类型 /// - [DisplayName("类型")] public string Type { get; set; } = ""; /// /// 获得/设置 可选值 /// - [DisplayName("可选值")] public string ValueList { get; set; } = ""; /// - /// 获得/设置 默认值 + /// 获得/设置 版本 + /// + public string Version { get; set; } = ""; + + /// + /// /// - [DisplayName("默认值")] public string DefaultValue { get; set; } = ""; } diff --git a/src/BootstrapBlazor.Server/Directory.Build.targets b/src/BootstrapBlazor.Server/Directory.Build.targets index 7e382f03fe4..4beb8250df6 100644 --- a/src/BootstrapBlazor.Server/Directory.Build.targets +++ b/src/BootstrapBlazor.Server/Directory.Build.targets @@ -1,9 +1,17 @@ - + + + + + + + + + diff --git a/src/BootstrapBlazor.Server/Services/ComponentAttributeCacheService.cs b/src/BootstrapBlazor.Server/Services/ComponentAttributeCacheService.cs new file mode 100644 index 00000000000..b7cb7406b85 --- /dev/null +++ b/src/BootstrapBlazor.Server/Services/ComponentAttributeCacheService.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Collections.Concurrent; +using System.Globalization; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; + +namespace BootstrapBlazor.Server.Services; + +/// +/// 组件属性缓存服务 +/// +public static class ComponentAttributeCacheService +{ + private static readonly ConcurrentDictionary> _cache = new(); + + /// + /// 通过组件类型获取组件的 AttributeItem 列表 + /// + public static List GetAttributes(Type componentType) + { +#if DEBUG + return GetAttributeCore(componentType); +#else + var currentLanguage = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + var key = $"{componentType.FullName}_{currentLanguage}"; + return _cache.GetOrAdd(key, _ => GetAttributeCore(componentType)); +#endif + } + + private static List GetAttributeCore(Type type) + { + var attributes = new List(); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.GetCustomAttribute() != null); + + var xmlDoc = GetXmlDocumentation(type.Assembly); + foreach (var property in properties) + { + var item = new AttributeItem + { + Name = property.Name, + Type = GetFriendlyTypeName(property.PropertyType), + Description = GetSummary(xmlDoc, property) ?? "", + Version = GetVersion(xmlDoc, property) ?? "10.0.0" + }; + attributes.Add(item); + } + return attributes.OrderBy(i => i.Name).ToList(); + } + + /// + /// 从 XML 注释获取 summary(支持多语言) + /// + private static string? GetSummary(XDocument? xmlDoc, PropertyInfo property) + { + if (xmlDoc == null) return null; + + var memberName = $"P:{property.DeclaringType?.FullName}.{property.Name}"; + var memberElement = xmlDoc.Descendants("member") + .FirstOrDefault(x => x.Attribute("name")?.Value == memberName); + + if (memberElement == null) return null; + + var summaryElement = memberElement.Element("summary"); + if (summaryElement == null) return null; + + // 获取当前语言(zh-CN -> zh, en-US -> en) + var currentLanguage = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + + // 查找匹配当前语言的 para 元素 + var langPara = summaryElement.Elements("para") + .FirstOrDefault(p => p.Attribute("lang")?.Value == currentLanguage); + + if (langPara != null) + { + return langPara.Value.Trim(); + } + + // 如果找不到当前语言,回退到第一个有 lang 属性的 para(通常是 zh) + var firstLangPara = summaryElement.Elements("para") + .FirstOrDefault(p => p.Attribute("lang") != null); + + if (firstLangPara != null) + { + return firstLangPara.Value.Trim(); + } + + // 如果都没有,返回整个 summary 的文本(向后兼容旧格式) + return summaryElement.Value.Trim(); + } + + /// + /// 从 XML 注释的 para version 节点获取版本信息 + /// + private static string? GetVersion(XDocument? xmlDoc, PropertyInfo property) + { + if (xmlDoc == null) return null; + + var memberName = $"P:{property.DeclaringType?.FullName}.{property.Name}"; + var memberElement = xmlDoc.Descendants("member") + .FirstOrDefault(x => x.Attribute("name")?.Value == memberName); + + if (memberElement == null) return null; + + // 在 summary 节点下查找包含 version 的 para 节点 + // XML 格式: 10.2.2 + var summaryElement = memberElement.Element("summary"); + if (summaryElement == null) return null; + + // 查找第一个包含 version 元素的 para + // 直接在循环中返回,避免创建中间变量 + return summaryElement.Elements("para") + .Select(p => p.Element("version")) + .FirstOrDefault(v => v != null) + ?.Value.Trim(); + } + + /// + /// 获取友好的类型名称 + /// + private static string GetFriendlyTypeName(Type type) + { + if (type.IsGenericType) + { + var genericTypeName = type.GetGenericTypeDefinition().Name; + var backtickIndex = genericTypeName.IndexOf('`'); + if (backtickIndex > 0) + { + genericTypeName = genericTypeName.Substring(0, backtickIndex); + } + var genericArgs = string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName)); + return $"{genericTypeName}<{genericArgs}>"; + } + + return type.Name switch + { + "Int32" => "int", + "String" => "string", + "Boolean" => "bool", + "Double" => "double", + "Decimal" => "decimal", + _ => type.Name + }; + } + + /// + /// 获取 XML 文档 + /// + private static XDocument? GetXmlDocumentation(Assembly? assembly) + { + if (assembly == null) return null; + + try + { + var assemblyLocation = assembly.Location; + if (string.IsNullOrEmpty(assemblyLocation)) + { + return null; + } + + var xmlPath = Path.Combine( + Path.GetDirectoryName(assemblyLocation) ?? "", + Path.GetFileNameWithoutExtension(assemblyLocation) + ".xml" + ); + + if (File.Exists(xmlPath)) + { + // 使用安全的 XML 读取设置防止 XXE 攻击 + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null + }; + + using var reader = XmlReader.Create(xmlPath, settings); + return XDocument.Load(reader); + } + } + catch (FileNotFoundException) + { + // XML 文档文件不存在,忽略 + } + catch (XmlException) + { + // XML 文档格式错误,忽略 + } + catch (UnauthorizedAccessException) + { + // 没有访问权限,忽略 + } + + return null; + } + + /// + /// 清除缓存 + /// + public static void ClearCache() + { + _cache.Clear(); + } +}