Hello Blazor:(9)Source Generators生成导航菜单

前言

最近写了多篇关于Source Generators的文章,发现它确实可以简化我们的部分开发工作。

这不,我又盯上了Blazor。

问题

默认的NavMenu.razor组件用于显示导航菜单,它的部分代码如下:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

这也就意味着,如果我们增加一个页面,就要修改一次NavMenu.razor组件,这当然是不合适的。

实现原理

我们查看obj\Debug\net5.0\Razor\Pages\Counter.razor.g.cs(编译时生成的中间文件),可以看到如下代码:

[Microsoft.AspNetCore.Components.RouteAttribute("/counter")]
public partial class Counter : Microsoft.AspNetCore.Components.ComponentBase

它其实对应源代码里的:

@page "/counter"

也就是说,只要我们遍历所有的Microsoft.AspNetCore.Components.RouteAttribute,获得路由信息放到List<Menu>即可。

具体实现代码如下:

[Generator]
    public class MenuGenerator : ISourceGenerator
    {
        private const string MenuClassText = @"
public class Menu
{
    public string Route { get; set; }
    public string Title { get; set; }
}";

        public void Initialize(GeneratorInitializationContext context)
        {
        }

        public void Execute(GeneratorExecutionContext context)
        {
            context.AddSource("Menu", SourceText.From(MenuClassText, Encoding.UTF8));

            var options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(MenuClassText, Encoding.UTF8), options));
             
            var allClasses = compilation.SyntaxTrees.
               SelectMany(x => x.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>());

            var sourceBuilder = new StringBuilder(@"
using System.Collections.Generic;
namespace MenuGenerator
{
    public static class NavHelper
    {
        public static IEnumerable<Menu> GetMenus(){
            return new List<Menu> {");
            foreach (var classDeclarationSyntax in allClasses)
            {
                var routeAttribute = classDeclarationSyntax.AttributeLists.SelectMany(x => x.Attributes).FirstOrDefault(attr => attr.Name.ToString() == "Microsoft.AspNetCore.Components.RouteAttribute");
                if (routeAttribute != null)
                {
                    var routeArg = routeAttribute.ArgumentList.Arguments.First();
                    var routeExpr = routeArg.Expression;
                    var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree);
                    var route = semanticModel.GetConstantValue(routeExpr).ToString();
                    if (route == @"/") continue;
                    var title = classDeclarationSyntax.Identifier.ToString();

                    sourceBuilder.Append($@" 
new Menu{{ Route = ""{route}"", Title = ""{title}"" }},");
                }
            }
            sourceBuilder.Append(@"
};
}
    }
}");

            context.AddSource("Mapper", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
        }
    }

使用示例

修改NavMenu.razor代码如下:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        @foreach (var menu in MenuGenerator.NavHelper.GetMenus())
        {
            <li class="nav-item px-3">
                <NavLink class="nav-link" href="@menu.Route">
                    <span class="oi " aria-hidden="true"></span> @menu.Title
                </NavLink>
            </li>
        }
    </ul>
</div>

编译后可以看到自动生成的代码:

运行后测试,工作正常,成功!

结论

菜单信息还有许多地方需要扩展,比如顺序、图标、菜单名称等,这些可以通过添加自定义Attribute实现。

如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值