هدف از اینترفیس INotifyPropertyChanged که به همراه یک رخداد است:
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; }
partial class CarModel : INotifyPropertyChanged { private double _speedKmPerHour; public double SpeedKmPerHour { get => _speedKmPerHour; set { _speedKmPerHour = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SpeedKmPerHour))); } } public event PropertyChangedEventHandler? PropertyChanged; }
اگر بخواهیم تولید این کدهای تکراری را به Source Generators محول کنیم، میتوان برای مثال فیلد خصوصی مرتبط را نگه داشت و تولید مابقی کدها را خودکار کرد:
partial class CarModel : INotifyPropertyChanged { private double _speedKmPerHour; }
ایجاد پروژهی Source Generator
در ابتدا برای ایجاد تولید کنندهی خودکار کدهای INotifyPropertyChanged، یک class library را به solution جاری اضافه میکنیم. سپس نیاز است ارجاعاتی را به دو بستهی نیوگت زیر نیز افزود:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" /> </ItemGroup> </Project>
[Generator] public class NotifyPropertyChangedGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) {
- اینترفیس ISourceGenerator به همراه دو متد Initialize و Execute است که در صورت نیاز باید پیاده سازی شوند.
در متد Execute، به خاصیت context.Compilation دسترسی داریم. این خاصیت تمام اطلاعاتی را که کامپایلر از Solution جاری در اختیار دارد، به توسعه دهنده ارائه میدهد. برای نمونه پیاده سازی متد Execute تولید کنندهی کد مثال جاری، چنین شکلی را دارد:
public void Execute(GeneratorExecutionContext context) { // uncomment to debug the actual build of the target project // Debugger.Launch(); var compilation = context.Compilation; var notifyInterface = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); foreach (var syntaxTree in compilation.SyntaxTrees) { var semanticModel = compilation.GetSemanticModel(syntaxTree); var immutableHashSet = syntaxTree.GetRoot() .DescendantNodesAndSelf() .OfType<ClassDeclarationSyntax>() .Select(x => semanticModel.GetDeclaredSymbol(x)) .OfType<ITypeSymbol>() .Where(x => x.Interfaces.Contains(notifyInterface)) .ToImmutableHashSet(); foreach (var typeSymbol in immutableHashSet) { var source = GeneratePropertyChanged(typeSymbol); context.AddSource($"{typeSymbol.Name}.Notify.cs", source); } } }
نکتهی مهم و جالب در اینجا این است که نیازی نیست تا قطعه کد جدید را به صورت SyntaxTrees در آورد و به کامپایلر اضافه کرد. میتوان این قطعه کد را به نحو متداولی، به صورت یک قطعه رشتهی استاندارد #C، تولید و به کامپایلر با متد context.AddSource ارائه کرد که نمونهای از آنرا در ذیل مشاهده میکنید:
private string GeneratePropertyChanged(ITypeSymbol typeSymbol) { return $@" using System.ComponentModel; namespace {typeSymbol.ContainingNamespace} {{ partial class {typeSymbol.Name} {{ {GenerateProperties(typeSymbol)} public event PropertyChangedEventHandler? PropertyChanged; }} }}"; } private static string GenerateProperties(ITypeSymbol typeSymbol) { var sb = new StringBuilder(); var suffix = "BackingField"; foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>() .Where(x=>x.Name.EndsWith(suffix))) { var propertyName = fieldSymbol.Name[..^suffix.Length]; sb.AppendLine($@" public {fieldSymbol.Type} {propertyName} {{ get => {fieldSymbol.Name}; set {{ {fieldSymbol.Name} = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName}))); }} }}"); } return sb.ToString(); }
کدهای source generator ما همین مقدار بیشتر نیست. اکنون میخواهیم از آن در یک برنامهی کنسول جدید (برای مثال به نام NotifyPropertyChangedGenerator.Demo) استفاده کنیم. برای اینکار نیاز است ارجاعی را به آن اضافه کنیم؛ اما این ارجاع، یک ارجاع متداول نیست و نیاز به ذکر چنین ویژگی خاصی وجود دارد:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> </ItemGroup> </Project>
برای آزمایش این تولید کنندهی کد، کلاس CarModel را به صورت زیر به پروژهی کنسول آزمایشی اضافه میکنیم:
using System.ComponentModel; namespace NotifyPropertyChangedGenerator.Demo { public partial class CarModel : INotifyPropertyChanged { private double SpeedKmPerHourBackingField; private int NumberOfDoorsBackingField; private string ModelBackingField = ""; public void SpeedUp() => SpeedKmPerHour *= 1.1; } }
نحوهی مشاهدهی کدهای خودکار تولید شده
اگر علاقمند باشید تا کدهای خودکار تولید شده را مشاهده کنید، در Visual Studio، در قسمت و درخت نمایشی dependencies پروژه، گرهای به نام Analyzers وجود دارد که در آن برای مثال نام NotifyPropertyChangedGenerator و ذیل آن، کلاسهای تولید شدهی توسط آن، قابل مشاهده و دسترسی هستند و حتی قابل دیباگ نیز میباشند؛ یعنی میتوان بر روی سطور مختلف آن، break-point قرار داد.
در اینجا تعدادی مثال را که توسط مایکروسافت توسعه یافتهاست، مشاهده میکنید که اتفاقا یکی از آنها پیاده سازی تولید کنندهی کد اینترفیس INotifyPropertyChanged است. در این برنامه، خروجی کدهای تولیدی نیز به سادگی قابل مشاهدهاست.
- برنامه SharpLab
برای توسعهی تولید کنندههای کد، عموما نیاز است تا با Roslyn API آشنا بود. در این برنامه اگر از منوی بالای صفحه قسمت results، گزینهی «syntax tree» را انتخاب کنید و سپس قسمتی از کد خود را انتخاب کنید، بلافاصله معادل Roslyn API آن، در سمت راست صفحه نمایش داده میشود.
- معرفی مجموعهای از Source Generators
در اینجا میتوان مجموعهای از پروژههای سورس باز Source Generators را مشاهده و کدهای آنها را مطالعه کنید و یا از آنها در پروژههای خود استفاده نمائید.
- معرفی یک cookbook در مورد Source Generators
این cookbook توسط خود مایکروسافت تهیه شدهاست و جهت شروع به کار با این فناوری، بسیار مفید است.
- مجموعه مثالهای Source generators از مایکروسافت
در اینجا میتوانید مجموعه مثالهایی از Source generators را که توسط مایکروسافت تهیه شدهاست، مشاهده کنید. شرح و توضیحات تعدادی از آنها را هم در اینجا مطالعه کنید.