我有三个 C# 项目,旨在使用自定义属性来生成引用使用自定义 IIncremental Generator 的公共类的代码。我使用并扩展了 dotnet 的本指南 https://www.youtube.com/watch?v=Yf8t7GqA6zA
公共图书馆项目有以下类:
public class TestRelayCommand<T> : IRelayCommand<T>
{
#region "Constructors..."
public TestRelayCommand(ViewModelBase viewModel, Action<T> execute)
{
ViewModel = viewModel;
_command = new(execute);
}
public TestRelayCommand(ViewModelBase viewModel, Action<T> execute, Predicate<T> canExecute)
{
ViewModel = viewModel;
_command = new(execute, canExecute);
_command.CanExecuteChanged += _command_CanExecuteChanged;
}
#endregion
#region "Properties..."
public ViewModelBase ViewModel { get; }
private RelayCommand<T> _command;
public event EventHandler? CanExecuteChanged;
#endregion
#region "Methods..."
public bool CanExecute(T? parameter)
{
return _command.CanExecute(parameter);
}
public bool CanExecute(object? parameter)
{
return _command.CanExecute(parameter);
}
public void Execute(object? parameter)
{
//Action Started
_command.Execute(parameter);
//Action Ended
}
public void Execute(T? parameter)
{
//Action Started
_command.Execute(parameter);
//Action Ended
}
public void NotifyCanExecuteChanged()
{
_command.NotifyCanExecuteChanged();
}
#endregion
#region "Events..."
private void _command_CanExecuteChanged(object? sender, EventArgs e)
{
CanExecuteChanged?.Invoke(this, e);
}
#endregion
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public class TestRelayCommandAttribute : Attribute
{
public string? CanExecute { get; init; }
public bool AllowConcurrentExecutions { get; init; }
public bool FlowExceptionsToTaskScheduler { get; init; }
public bool IncludeCancelCommand { get; init; }
}
生成器项目具有以下生成器类:
[Generator(LanguageNames.CSharp)]
public class TestRelayCommandGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorInitialize.txt", "Started");
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
//context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
var provider = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is MethodDeclarationSyntax,
transform: static (ctx, _) => (MethodDeclarationSyntax)ctx.Node
).Where(m => m != null);// && m.AttributeLists.SelectMany(att => att.Attributes).Any(att => att.Name.ToString() == nameof(TestRelayCommand) || att.Name.ToString() == nameof(TestRelayCommandAttribute)));
var compilation = context.CompilationProvider.Combine(provider.Collect());
context.RegisterSourceOutput(compilation, Execute);
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorInitialized.txt", "Done Startup");
}
public void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray<MethodDeclarationSyntax> Right) compilation)
{
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorExecuting.txt", "Executing");
foreach (var method in compilation.Right)
{
var semanticModel = compilation.Left.GetSemanticModel(method.SyntaxTree);
var methodSymbol = semanticModel.GetDeclaredSymbol(method) as IMethodSymbol;
if (methodSymbol == null || !methodSymbol.GetAttributes().Any())
continue;
var attributeData = methodSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "TestRelayCommand");
var canExecuteArg = attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == nameof(TestRelayCommandAttribute.CanExecute));
string canExecute = canExecuteArg.Value.Value != null ? canExecuteArg.Value.Value.ToString() : null;
if (!string.IsNullOrEmpty(canExecute)) canExecute = ", " + canExecute;
var classDeclaration = method.FirstAncestorOrSelf<ClassDeclarationSyntax>();
if (classDeclaration == null)
continue;
var namespaceDeclaration = classDeclaration.FirstAncestorOrSelf<NamespaceDeclarationSyntax>();
var namespaceName = namespaceDeclaration?.Name.ToString();
var className = classDeclaration.Identifier.Text;
var methodName = method.Identifier.Text;
var parameters = string.Join(", ", methodSymbol.Parameters.Select(p => $"{p.Type}"));
if (!string.IsNullOrEmpty(parameters)) parameters = "<" + parameters + ">";
//var returnType = methodSymbol.ReturnType.ToString();
//if (returnType != "void")
// returnType = "<" + returnType + ">";
//else
// returnType = "";
var source = $@"
space {namespaceName}
public partial class {className}
{{
private WPF.UI.Analyzer.{nameof(TestRelayCommand)}{parameters} _{methodName.Substring(0, 1).ToLower()}{methodName.Substring(1)}Command;
public WPF.UI.Analyzer.{nameof(TestRelayCommand)}{parameters} {methodName}Command => _{methodName.Substring(0, 1).ToLower()}{methodName.Substring(1)}Command ??= new(this, new({methodName}){canExecute});
}}
context.AddSource($"{className}_{methodName}_{nameof(TestRelayCommand)}.g.cs", SourceText.From(source, Encoding.UTF8));
File.WriteAllText("C:\\Users\\LMatheson\\Desktop\\Generator.txt", source);
}
}
}
使用公共属性的最终项目引用生成器项目,如下所示:
<ProjectReference Include="..\WPF.UI.Analyzer\WPF.UI.Analyzer.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
并且包含具有属性的此类:
public partial class UserRoleListViewModel
{
[TestRelayCommand]
public override async void SelectedViewModelChanging(object args)
{
Debug.Print("Testing");
}
}
生成器应该创建一个与方法的父级匹配的分部类,具有 TestRelayCommand 类型的字段和属性
该项目需要使用.Net Standard 2.0作为框架来构建。任何较新的框架都会使任何生成器无法运行。
切换现有的 .Net 8 项目未能使分析器运行,因此我创建了一个新项目并遵循 Andrew Lock 的指南(此处找到:https://andrewlock.net/creating-a-source-generator-part- 1-creating-an-incremental-source-generator/) 来格式化 .csproj 文件,重新添加我之前项目中的生成器,并将这个新项目作为引用包含在包含它查找的属性的项目中。经过这些步骤后,它就能够分析项目并输出预期的代码。