Loading

使用 roslyn 的 Source Generator 自动完成依赖收集和注册

使用 Hosting 构建 WPF 程序 提到,因为不使用 Stylet 默认的 IOC 容器,所以不能自动收集和注册 View/ViewModel,需要动手处理。

如果项目比较大,手动处理显然过于麻烦。这里使用 roslyn 的 Source Generator 自动完成依赖收集和注册。

源码 JasonGrass/WpfAppTemplate1: WPF + Stylet + Hosting

新建分析器项目

以类库的模板,新建 WpfAppTemplate1.Generators,或者直接使用 Rider 新建。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <IsPackable>false</IsPackable>
        <Nullable>enable</Nullable>
        <LangVersion>latest</LangVersion>

        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
        <IsRoslynComponent>true</IsRoslynComponent>

        <RootNamespace>WpfAppTemplate1.Generators</RootNamespace>
        <PackageId>WpfAppTemplate1.Generators</PackageId>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
    </ItemGroup>

</Project>

编写 SourceGenerator 代码

新建一个类,继承自 ISourceGenerator,并添加 Generator Attribute。

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace WpfAppTemplate1.Generators;

[Generator]
public class ViewDependencyInjectionGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context) { }

    public void Execute(GeneratorExecutionContext context)
    {
        // System.Diagnostics.Debugger.Launch();

        // 获取所有语法树
        var compilation = context.Compilation;
        var syntaxTrees = context.Compilation.SyntaxTrees;

        // 查找目标类型(ViewModel和View)
        var clsNodeList = syntaxTrees
            .SelectMany(tree => tree.GetRoot().DescendantNodes())
            .OfType<ClassDeclarationSyntax>()
            .Where(cls =>
                cls.Identifier.Text.EndsWith("ViewModel") || cls.Identifier.Text.EndsWith("View")
            )
            .Select(cls => new
            {
                ClassDeclaration = cls,
                ModelSymbol = compilation.GetSemanticModel(cls.SyntaxTree).GetDeclaredSymbol(cls),
            })
            .ToList();

        // 生成注册代码
        var sourceBuilder = new StringBuilder(
            @"
using Microsoft.Extensions.DependencyInjection;

public static class ViewModelDependencyInjection
{
    public static void AddViewModelServices(this IServiceCollection services)
    {
"
        );

        HashSet<string> added = new HashSet<string>();

        foreach (var clsNode in clsNodeList)
        {
            if (clsNode.ModelSymbol == null)
            {
                continue;
            }

            // var namespaceName = type.ModelSymbol.ContainingNamespace.ToDisplayString();
            var fullName = clsNode.ModelSymbol.ToDisplayString(); // 包含命名空间的全称

            if (!added.Add(fullName))
            {
                // 避免因为 partial class 造成的重复添加
                continue;
            }

            // ViewModel 必须继承 Stylet.Screen
            if (
                clsNode.ClassDeclaration.Identifier.Text.EndsWith("ViewModel")
                && InheritsFrom(clsNode.ModelSymbol, "Stylet.Screen")
            )
            {
                sourceBuilder.AppendLine($"        services.AddSingleton<{fullName}>();");
            }
            // View 必须继承 System.Windows.FrameworkElement
            else if (
                clsNode.ClassDeclaration.Identifier.Text.EndsWith("View")
                && InheritsFrom(clsNode.ModelSymbol, "System.Windows.FrameworkElement")
            )
            {
                sourceBuilder.AppendLine($"        services.AddSingleton<{fullName}>();");
            }
        }

        sourceBuilder.AppendLine("    }");
        sourceBuilder.AppendLine("}");

        var code = sourceBuilder.ToString();

        // 添加生成的代码到编译过程
        context.AddSource(
            "ViewModelDependencyInjection.g.cs",
            SourceText.From(code, Encoding.UTF8)
        );
    }

    private bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseClassName)
    {
        while (typeSymbol.BaseType != null)
        {
            if (typeSymbol.BaseType.ToDisplayString() == baseClassName)
            {
                return true;
            }
            typeSymbol = typeSymbol.BaseType;
        }
        return false;
    }
}

最终生成的代码如下:

using Microsoft.Extensions.DependencyInjection;

public static class ViewModelDependencyInjection
{
    public static void AddViewModelServices(this IServiceCollection services)
    {
        services.AddSingleton<WpfAppTemplate1.View.RootView>();
        services.AddSingleton<WpfAppTemplate1.ViewModel.RootViewModel>();
    }
}

这里没有指定命名空间,直接使用默认的命名空间。

在 WpfAppTemplate1 项目中使用

这里没有生成 nuget 包,直接使用项目引用

  <ItemGroup>
    <ProjectReference Include="..\WpfAppTemplate1.Generators\WpfAppTemplate1.Generators.csproj"  OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
  </ItemGroup>

OutputItemType="Analyzer" 表示将项目添加为分析器
ReferenceOutputAssembly="false" 表示此项目无需引用分析器项目的程序集

然后,在 Bootstrapper 中调用

protected override void ConfigureIoC(IServiceCollection services)
{
    base.ConfigureIoC(services);
    // services.AddSingleton<RootViewModel>();
    // services.AddSingleton<RootView>();

    services.AddViewModelServices();
}

至此,大功告成。

可以在这里找到自动生成的代码

image

几个问题

1 编写完成之后没有生效

VS 对代码生成器的支持看起来还不是很好,尝试重启 VS;或者直接使用 Rider。

2 调试 source generator

对于新建的 source generator 项目,rider 会自动生成 launchSettings.json,可以直接启动项目进行调试

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "DebugRoslynSourceGenerator": {
      "commandName": "DebugRoslynComponent",
      "targetProject": "../WpfAppTemplate1/WpfAppTemplate1.csproj"
    }
  }
}

image

参考

SamplesInPractice/SourceGeneratorSample at main · WeihanLi/SamplesInPractice

使用 Source Generator 在编译你的 .NET 项目时自动生成代码 - walterlv

.net - C# Source Generator - warning CS8032: An instance of analyzer cannot be created - Stack Overflow

C# 源代码生成器的痛点:2022 年 2 月更新 - Turnerj(又名 James Turner) --- The pain points of C# source generators: February 2022 Update - Turnerj (aka. James Turner)

原文链接:https://www.cnblogs.com/jasongrass/p/18540540

posted @ 2024-11-11 20:47  J.晒太阳的猫  阅读(4)  评论(2编辑  收藏  举报