Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
@@ -1,4 +1,4 @@
<div class="table-attr">
<div class="table-attr">
<h4>@Title</h4>

<Table TItem="AttributeItem" Items="Items">
Expand All @@ -8,6 +8,7 @@
<TableColumn @bind-Field="@context.Type" />
<TableColumn @bind-Field="@context.ValueList" TextWrap="true" />
<TableColumn @bind-Field="@context.DefaultValue" />
<TableColumn @bind-Field="@context.Version" />
</TableColumns>
</Table>
</div>
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -26,6 +26,12 @@ public sealed partial class AttributeTable
/// </summary>
[Parameter] public IEnumerable<AttributeItem>? Items { get; set; }

/// <summary>
///
Comment thread
ArgoZhang marked this conversation as resolved.
Outdated
/// </summary>
[Parameter]
public Type? Type { get; set; }

/// <summary>
/// OnInitialized 方法
/// </summary>
Expand All @@ -34,5 +40,10 @@ protected override void OnInitialized()
base.OnInitialized();

Title ??= Localizer[nameof(Title)];

if (Items == null && Type != null)
{
Items = ComponentAttributeCacheService.GetAttributes(Type);
}
}
}
22 changes: 12 additions & 10 deletions src/BootstrapBlazor.Server/Components/Samples/Circles.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@

<DemoBlock Title="@Localizer["BasicUsageTitle"]" Introduction="@Localizer["BasicUsageIntro"]" Name="Normal">
<Circle Value="@_circleValue" />
<section ignore class="btn-group mt-3 d-block">
<button class="btn btn-primary" @onclick="e => Add(10)">
<i class="fa-solid fa-plus"></i>
<span>@Localizer["IncreaseSpan"] 10</span>
</button>
<button class="btn btn-info" @onclick="e => Add(-10)">
<i class="fa-solid fa-minus"></i>
<span>@Localizer["DecreaseSpan"] 10</span>
</button>
<section ignore class="mt-3">
<div class="btn-group">
<button class="btn btn-primary" @onclick="e => Add(10)">
<i class="fa-solid fa-plus"></i>
<span>@Localizer["IncreaseSpan"] 10</span>
</button>
<button class="btn btn-info" @onclick="e => Add(-10)">
<i class="fa-solid fa-minus"></i>
<span>@Localizer["DecreaseSpan"] 10</span>
</button>
</div>
</section>
</DemoBlock>

Expand Down Expand Up @@ -53,4 +55,4 @@
</Circle>
</DemoBlock>

<AttributeTable Items="@GetAttributes()" />
<AttributeTable Type="@typeof(Circle)" />
60 changes: 8 additions & 52 deletions src/BootstrapBlazor.Server/Components/Samples/Circles.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

using BootstrapBlazor.Server.Services;

namespace BootstrapBlazor.Server.Components.Samples;

/// <summary>
Expand All @@ -19,59 +21,13 @@ private void Add(int interval)
}

/// <summary>
/// GetAttributes
/// GetAttributes - 使用反射和缓存自动获取 Circle 组件属性
/// </summary>
/// <returns></returns>
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 = ""
}
];
private AttributeItem[] GetAttributes()
{
// 通过示例组件名称 Circles 确定组件类型为 Circle
return ComponentAttributeCacheService.GetAttributes(typeof(BootstrapBlazor.Components.Circle));
}
Comment thread
ArgoZhang marked this conversation as resolved.
Outdated
}

6 changes: 6 additions & 0 deletions src/BootstrapBlazor.Server/Data/AttributeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ public class AttributeItem
/// </summary>
[DisplayName("默认值")]
public string DefaultValue { get; set; } = "";

/// <summary>
/// 获得/设置 版本
/// </summary>
[DisplayName("版本")]
public string Version { get; set; } = "";
}
10 changes: 9 additions & 1 deletion src/BootstrapBlazor.Server/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<Project>

<Target Name="LLMs" AfterTargets="Publish" Condition="'$(TargetFramework)' == '$(RunTargetFramework)'">
<Target Name="CopyXmlDocs" AfterTargets="Publish">
<ItemGroup>
<XmlDocsToCopy Include="%(Reference.RelativeDir)BootstrapBlazor*.xml" />
Comment thread
ArgoZhang marked this conversation as resolved.
</ItemGroup>
<Message Text="Copying XML docs to output path: $(PublishDir)" Importance="High" />
<Copy SourceFiles="@(XmlDocsToCopy)" DestinationFolder="$(PublishDir)" SkipUnchangedFiles="true" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
</Target>

<Target Name="LLMs" AfterTargets="CopyXmlDocs">
<Message Text="LLMs documentation generating ..." Importance="high"></Message>
<Exec Command="dotnet tool restore"></Exec>
<Exec Command="dotnet llms-docs --root=$(MSBuildThisFileDirectory) --output=$(PublishDir)"></Exec>
Expand Down
206 changes: 206 additions & 0 deletions src/BootstrapBlazor.Server/Services/ComponentAttributeCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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.ComponentModel;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;

namespace BootstrapBlazor.Server.Services;

/// <summary>
/// 组件属性缓存服务
/// </summary>
public static class ComponentAttributeCacheService
{
private static readonly ConcurrentDictionary<Type, AttributeItem[]> _cache = new();

/// <summary>
/// 获取组件的 AttributeItem 列表(带缓存)
/// </summary>
public static AttributeItem[] GetAttributes(Type componentType)
{
return _cache.GetOrAdd(componentType, type =>
{
var attributes = new List<AttributeItem>();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetCustomAttribute<ParameterAttribute>() != null);

foreach (var property in properties)
{
var item = new AttributeItem
{
Name = property.Name,
Type = GetFriendlyTypeName(property.PropertyType),
Description = GetSummary(property) ?? "",
DefaultValue = GetDefaultValue(property) ?? "",
ValueList = GetValueList(property) ?? "",
Version = GetVersion(property) ?? ""
};
attributes.Add(item);
}

return attributes.ToArray();
});
}

/// <summary>
/// 从 XML 注释获取 summary
/// </summary>
private static string? GetSummary(PropertyInfo property)
{
var xmlDoc = GetXmlDocumentation(property.DeclaringType?.Assembly);
if (xmlDoc == null) return null;

var memberName = $"P:{property.DeclaringType?.FullName}.{property.Name}";
var element = xmlDoc.Descendants("member")
.FirstOrDefault(x => x.Attribute("name")?.Value == memberName);

return element?.Element("summary")?.Value.Trim();
}

/// <summary>
/// 从 XML 注释的 para version 节点获取版本信息
/// </summary>
private static string? GetVersion(PropertyInfo property)
{
var xmlDoc = GetXmlDocumentation(property.DeclaringType?.Assembly);
if (xmlDoc == null) return null;

var memberName = $"P:{property.DeclaringType?.FullName}.{property.Name}";
var element = xmlDoc.Descendants("member")
.FirstOrDefault(x => x.Attribute("name")?.Value == memberName);

// 查找 <para><version>10.0.0</version></para>
var versionElement = element?.Descendants("para")
.SelectMany(p => p.Elements("version"))
.FirstOrDefault();

return versionElement?.Value.Trim();
}

/// <summary>
/// 获取默认值
/// </summary>
private static string? GetDefaultValue(PropertyInfo property)
{
var defaultValueAttr = property.GetCustomAttribute<DefaultValueAttribute>();
if (defaultValueAttr != null)
{
return defaultValueAttr.Value?.ToString() ?? "";
}

// 从 XML 注释中提取 DefaultValue
var xmlDoc = GetXmlDocumentation(property.DeclaringType?.Assembly);
if (xmlDoc == null) return null;

var memberName = $"P:{property.DeclaringType?.FullName}.{property.Name}";
var element = xmlDoc.Descendants("member")
.FirstOrDefault(x => x.Attribute("name")?.Value == memberName);

var defaultElement = element?.Element("value");
return defaultElement?.Value.Trim();
}

/// <summary>
/// 获取可选值列表
/// </summary>
private static string? GetValueList(PropertyInfo property)
{
// 如果是枚举类型,返回枚举值
if (property.PropertyType.IsEnum)
{
return string.Join("/", Enum.GetNames(property.PropertyType));
Comment thread
ArgoZhang marked this conversation as resolved.
Outdated
}

return null;
}

/// <summary>
/// 获取友好的类型名称
/// </summary>
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
};
}

/// <summary>
/// 获取 XML 文档
/// </summary>
private static XDocument? GetXmlDocumentation(Assembly? assembly)
Comment thread
ArgoZhang marked this conversation as resolved.
{
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;
}

/// <summary>
/// 清除缓存
/// </summary>
public static void ClearCache()
{
_cache.Clear();
}
}
Loading