Add demo source generator
MODIFY
WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.sln
WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.sln
@@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalletWasabi.Fluent", "..\W
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalletWasabi.Fluent.Desktop", "WalletWasabi.Fluent.Desktop.csproj", "{9E31DD9E-2E33-4FE7-991E-7A4054460E14}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletWasabi.Fluent.Generators", "..\WalletWasabi.Fluent.Generators\WalletWasabi.Fluent.Generators.csproj", "{F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -105,6 +107,14 @@ Global
{9E31DD9E-2E33-4FE7-991E-7A4054460E14}.Release|Any CPU.Build.0 = Release|Any CPU
{9E31DD9E-2E33-4FE7-991E-7A4054460E14}.Release|x64.ActiveCfg = Release|Any CPU
{9E31DD9E-2E33-4FE7-991E-7A4054460E14}.Release|x64.Build.0 = Release|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Debug|x64.Build.0 = Debug|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Release|x64.ActiveCfg = Release|Any CPU
+ {F9C06F4D-2517-4DBC-8F07-CAAEC0AD5B59}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

ADD
WalletWasabi.Fluent.Generators/Class1.cs
+using System;
+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 WalletWasabi.Fluent.Generators
+{
+ [Generator]
+ public class AutoNotifyGenerator : ISourceGenerator
+ {
+ private const string AttributeText = @"
+using System;
+namespace AutoNotify
+{
+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+ sealed class AutoNotifyAttribute : Attribute
+ {
+ public AutoNotifyAttribute()
+ {
+ }
+ public string PropertyName { get; set; }
+ }
+}
+";
+
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ //System.Diagnostics.Debugger.Launch();
+
+ // Register a syntax receiver that will be created for each generation pass
+ context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ // add the attribute text
+ context.AddSource("AutoNotifyAttribute", SourceText.From(AttributeText, Encoding.UTF8));
+
+ // retreive the populated receiver
+ if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
+ {
+ return;
+ }
+
+ // we're going to create a new compilation that contains the attribute.
+ // TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
+ CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
+ Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(AttributeText, Encoding.UTF8), options));
+
+ // get the newly bound attribute, and INotifyPropertyChanged
+ INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
+ INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
+
+ // loop over the candidate fields, and keep the ones that are actually annotated
+ List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
+ foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
+ {
+ SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
+ foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
+ {
+ // Get the symbol being decleared by the field, and keep it if its annotated
+ IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
+ if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
+ {
+ fieldSymbols.Add(fieldSymbol);
+ }
+ }
+ }
+
+ // group the fields by class, and generate the source
+ foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
+ {
+ string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
+ context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
+ }
+ }
+
+ private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, GeneratorExecutionContext context)
+ {
+ if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
+ {
+ return null; //TODO: issue a diagnostic that it must be top level
+ }
+
+ string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
+
+ // begin building the generated source
+ StringBuilder source = new StringBuilder($@"
+namespace {namespaceName}
+{{
+ public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
+ {{
+");
+
+ // if the class doesn't implement INotifyPropertyChanged already, add it
+ if (!classSymbol.Interfaces.Contains(notifySymbol))
+ {
+ source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
+ }
+
+ // create properties for each field
+ foreach (IFieldSymbol fieldSymbol in fields)
+ {
+ ProcessField(source, fieldSymbol, attributeSymbol);
+ }
+
+ source.Append("} }");
+ return source.ToString();
+ }
+
+ private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
+ {
+ // get the name and type of the field
+ string fieldName = fieldSymbol.Name;
+ ITypeSymbol fieldType = fieldSymbol.Type;
+
+ // get the AutoNotify attribute from the field, and any associated data
+ AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
+ TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;
+
+ string propertyName = ChooseName(fieldName, overridenNameOpt);
+ if (propertyName.Length == 0 || propertyName == fieldName)
+ {
+ //TODO: issue a diagnostic that we can't process this field
+ return;
+ }
+
+ source.Append($@"
+public {fieldType} {propertyName}
+{{
+ get
+ {{
+ return this.{fieldName};
+ }}
+ set
+ {{
+ this.{fieldName} = value;
+ this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
+ }}
+}}
+");
+
+ static string ChooseName(string fieldName, TypedConstant overridenNameOpt)
+ {
+ if (!overridenNameOpt.IsNull)
+ {
+ return overridenNameOpt.Value.ToString();
+ }
+
+ fieldName = fieldName.TrimStart('_');
+ if (fieldName.Length == 0)
+ {
+ return string.Empty;
+ }
+
+ if (fieldName.Length == 1)
+ {
+ return fieldName.ToUpper();
+ }
+
+#pragma warning disable IDE0057 // Use range operator
+ return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
+#pragma warning restore IDE0057 // Use range operator
+ }
+
+ }
+
+ /// <summary>
+ /// Created on demand before each generation pass
+ /// </summary>
+ class SyntaxReceiver : ISyntaxReceiver
+ {
+ public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
+
+ /// <summary>
+ /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
+ /// </summary>
+ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+ {
+ // any field with at least one attribute is a candidate for property generation
+ if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
+ && fieldDeclarationSyntax.AttributeLists.Count > 0)
+ {
+ CandidateFields.Add(fieldDeclarationSyntax);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file

ADD
WalletWasabi.Fluent.Generators/WalletWasabi.Fluent.Generators.csproj
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AnalysisLevel>latest</AnalysisLevel>
+ <Nullable>disable</Nullable>
+ <LangVersion>latest</LangVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" />
+ </ItemGroup>
+
+</Project>

MODIFY
WalletWasabi.Fluent/ViewModels/AboutViewModel.cs
WalletWasabi.Fluent/ViewModels/AboutViewModel.cs
@@ -9,11 +9,15 @@ using WalletWasabi.Fluent.ViewModels.Dialogs;
using WalletWasabi.Fluent.ViewModels.Navigation;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
+using AutoNotify;
namespace WalletWasabi.Fluent.ViewModels
{
- public class AboutViewModel : RoutableViewModel
+ public partial class AboutViewModel : RoutableViewModel
{
+ [AutoNotify]
+ private bool _test;
+
public AboutViewModel()
{
OpenBrowserCommand = ReactiveCommand.CreateFromTask<string>(IoHelpers.OpenBrowserAsync);

MODIFY
WalletWasabi.Fluent/WalletWasabi.Fluent.csproj
WalletWasabi.Fluent/WalletWasabi.Fluent.csproj
<ItemGroup>
<ProjectReference Include="..\WalletWasabi.Gui\WalletWasabi.Gui.csproj" />
<ProjectReference Include="..\WalletWasabi\WalletWasabi.csproj" />
+ <ProjectReference Include="..\WalletWasabi.Fluent.Generators\WalletWasabi.Fluent.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

Add demo source generator