在 使用 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>
新建一个类,继承自 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>(); }}
这里没有指定命名空间,直接使用默认的命名空间。
这里没有生成 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();}
至此,大功告成。
可以在这里找到自动生成的代码
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" } }}
来自徳熙大佬的提示: 现在 VisualStudio 团队推荐使用增量的源代码生成器,因为现在这篇博客使用的源代码生成器让原本就卡慢的 Visual Studio 更加卡慢了。 新的增量源代码生成器是很好饯行不可变和增量模式的写法,可以使用更少的资源
尝试 IIncrementalGenerator 进行增量 Source Generator 生成代码 | 林德熙
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Text;
namespace WpfAppTemplate1.Generators;
/* * 使用 IIncrementalGenerator 实现,优化 VS 调用性能 */
[Generator]internal class ViewDependencyInjectionGenerator2 : IIncrementalGenerator{ public void Initialize(IncrementalGeneratorInitializationContext context) { // 注册一个语法接收器,筛选出所有以 View 或 ViewModel 结尾的类声明 var classDeclarations = context .SyntaxProvider.CreateSyntaxProvider( predicate: IsCandidateClass, // 先通过语法筛选 transform: GetSemanticTarget // 再通过语义筛选 ) .Where(symbolAndClass => symbolAndClass.Symbol != null); // 过滤掉不符合条件
// 收集所有符合条件的类的全名 var classFullNames = classDeclarations .Select((symbolAndClass, ct) => symbolAndClass.Symbol!.ToDisplayString()) .Collect();
// 当收集完成后,进行代码的生成 context.RegisterSourceOutput( classFullNames, (spc, fullNames) => { if (fullNames.IsDefault || !fullNames.Any()) { // 如果没有符合条件的类,则不生成任何代码 return; }
var sourceBuilder = new StringBuilder( @"using Microsoft.Extensions.DependencyInjection;
public static class ViewModelDependencyInjection{ public static void AddViewModelServices(this IServiceCollection services) {" );
// 使用 HashSet 来避免重复添加 HashSet<string> added = new HashSet<string>();
foreach (var fullName in fullNames.Distinct()) { if (added.Add(fullName)) { sourceBuilder.AppendLine($" services.AddSingleton<{fullName}>();"); } }
sourceBuilder.AppendLine(" }"); sourceBuilder.AppendLine("}");
// 将生成的代码添加到编译过程中 spc.AddSource( "ViewModelDependencyInjection.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8) ); } ); }
/// <summary> /// 判断一个类声明是否是潜在的候选者(名称以 View 或 ViewModel 结尾) /// </summary> private static bool IsCandidateClass(SyntaxNode node, CancellationToken _) { return node is ClassDeclarationSyntax classDecl && ( classDecl.Identifier.Text.EndsWith("View") || classDecl.Identifier.Text.EndsWith("ViewModel") ); }
/// <summary> /// 获取符合条件的类的符号信息 /// </summary> private static (INamedTypeSymbol? Symbol, ClassDeclarationSyntax? ClassDecl) GetSemanticTarget( GeneratorSyntaxContext context, CancellationToken ct ) { var classDecl = (ClassDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
if (symbol == null) return (null, null);
// 检查继承关系 if (classDecl.Identifier.Text.EndsWith("ViewModel")) { // ViewModel 必须继承 Stylet.Screen if (!InheritsFrom(symbol, "Stylet.Screen")) return (null, null); } else if (classDecl.Identifier.Text.EndsWith("View")) { // View 必须继承 System.Windows.FrameworkElement if (!InheritsFrom(symbol, "System.Windows.FrameworkElement")) return (null, null); } else { return (null, null); }
return (symbol, classDecl); }
/// <summary> /// 判断一个符号是否继承自指定的基类 /// </summary> private static bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseClassFullName) { var current = typeSymbol.BaseType; while (current != null) { if (current.ToDisplayString() == baseClassFullName) { return true; } current = current.BaseType; } return false; }}
SamplesInPractice/SourceGeneratorSample at main · WeihanLi/SamplesInPractice
使用 Source Generator 在编译你的 .NET 项目时自动生成代码 - walterlv
原文链接: https://cloud.tencent.com/developer/article/2481578
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。