Source Generator For INotifyPropertyChanged

As a WPF Developer, one of the things which you would often have to do is to implement INotifyPropertyChange and ensure your properties raises the NotifyPropertyChange for each change (typically called in the setter). This is most often boilerplate code and it would have been great if this was taken care of, without the developer spending time in plumbing this boilerplate code. This also brings in the advantage of having a cleaner code base, rather than one cluttered with a lot of similar-looking code.

The Problem

For example, consider the following code,

public string FirstName
{
    get=> _firstName;
    set
    {
        if(_firstName == value) return;
        _firstName = value;
        NotifyPropertyChange();
    }
}

public string LastName
{
    get=> _lastName;
    set
    {
        if(_lastName == value) return;
        _lastName = value;
        NotifyPropertyChange();
    }
}

That is quite a lot of repetitive boilerplate code and as developers, we would love to get rid of them. Fody and PostSharp have been doing this for years now, but the introduction of Source Generator had to give developers a whole new option. In this blog post, we will build an AutoNotifyGenerator which would accomplish the same for us.

What we want to achieve

As mentioned earlier, the goal of the generator would be to reduce the plumbing of boilerplate code. To achieve this, we would allow the developers to decorate the backing fields (fields - not properties, we would be generating the properties using Generator) he is interested in with a special attribute.

This Generator on its part would iterate over each of the fields declarations in the class and select the fields which have the required attribute. It would then generate the property for the field, which would include a call to the NotifyPropertyChanged.

We would also need to verify that if the class (or any of its base classes) already implemented INotifyOfPropertyChanged. If it hasn't the Generator would have an additional responsibility to generate the source for implementing the interface as well.

AutoNotifyAttribute

The first task would be to create an attribute that would be used in our fields.

using System;

namespace InGen.Types.Attributes
{
    [AttributeUsage(AttributeTargets.Field,Inherited =false,AllowMultiple =false)]
    public class AutoNotifyAttribute:Attribute
    {
    }
}

The attribute, as expected doesn't do anything fancy themselves. They would be used to decorate the fields as follows.

namespace InGen.Client
{
    public partial class AutoNotifyGeneratorTests
    {
        [AutoNotify]
        private string _fullName;
        [AutoNotify]
        private string _displayName;

        private string _userName;

        [AutoNotify]
        private int _age;

    }
}

Syntax Reciever

The next step towards building our Generator would be to create a Syntax Receiver. In this particular case, we would create an implementation of `ISyntaxContextReciever` because we are interested in the `Context` to retrieve information about the attribute.

private class FieldSyntaxReciever : ISyntaxContextReceiver
{
    public List<IFieldSymbol> IdentifiedFields { get; } = new List<IFieldSymbol>();

    public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
    {
        if (context.Node is FieldDeclarationSyntax fieldDeclaration && fieldDeclaration.AttributeLists.Any())
        {
            var variableDeclaration = fieldDeclaration.Declaration.Variables;
            foreach(var variable in variableDeclaration)
            {
                var field = context.SemanticModel.GetDeclaredSymbol(variable);
                if (field is IFieldSymbol fieldInfo && fieldInfo.GetAttributes().Any(x=>x.AttributeClass.ToDisplayString() == "InGen.Types.Attributes.AutoNotifyAttribute"))
                {
                    IdentifiedFields.Add(fieldInfo);
                }
            }

        }
    }
}

During the compilation process, the compiler would invoke the `SyntaxReceiverCreator` prior to the generation to obtain an instance of `ISyntaxReciever`. The instance of `ISyntaxReciever` would inturn invoke `ISyntaxContextReciever.OnVisitSyntaxNode` for each node during the compilation and hence proves to be an excellent place to build the information required by the Syntax receiver for our Generator.

In the above code, we have checked if the current node is a Field Declaration and if so, check if it has an attribute of type `InGen.Types.Attributes.AutoNotifyAttribute`. For each of the fields which matches the condition, we add it to a list for our Generator to access.

Initialize Generator

At this point, we are ready to start working on our Generator. Let us call our Generator `AutoNotifyPropertyGenerator`.

[Generator]
public class AutoNotifyPropertyGenerator : ISourceGenerator
{
}

As observed our Generated needs to be decorated with a `GeneratorAttribute` and need to implement the `ISourceGenerator` interface. The `ISourceGenerator` interface consists of two methods - `Initialize` and `Execute`.

public interface ISourceGenerator
{
    void Execute(GeneratorExecutionContext context);
    void Initialize(GeneratorInitializationContext context);
}

Each Generator can register callbacks that are required for the successful completion of the Generator. This could be done using an instance of `GeneratorInitializationContext` which can be accessed using the `Initialize` method. The initialize method is called prior to the Generation of code.

We would use the Context instance in the `Initialize` method to register for Syntax Notifications using the Syntax Notifier we had declared previously.

public void Initialize(GeneratorInitializationContext context)
{
    context.RegisterForSyntaxNotifications(() => new FieldSyntaxReciever());
}

Generate Source

The remaining method in the interface, `Execute` is where the actual source generation is performed. We will slow down a bit so that we could understand each step better with the actual source generation.

Let us take a step back to figure out what we have done till now and what we need to do in the Execute method. We have created `SyntaxReciever` for detecting the fields with the required attributes. Then we used an instance of this SyntaxReciever as our SyntaxContextRecieverCreator to register the callback for Syntax Notifications. So what is left to do?

Here is an outline of what is left to do.

  • Create a partial representation of the class with the same namespace as the container class of our fields.
  • Check if the INotifyPropertyChanged has already been implemented. If not, implement it.
  • Iterated over the field and create properties with Setters invoking `NotifyPropertyChanged`.

Let us go ahead and write the code for doing the same.

public void Execute(GeneratorExecutionContext context) {
    if (context.SyntaxContextReceiver is not FieldSyntaxReciever syntaxReciever) return;
    var sourceBuilder = new StringBuilder();
    var notifySymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
    foreach(var containingClassGroup in syntaxReciever.IdentifiedFields.GroupBy(x => x.ContainingType)) {
        var containingClass = containingClassGroup.Key;
        var namespc = containingClass.ContainingNamespace;
        var hasNotifyImplementtion = containingClass.Interfaces.Contains(notifySymbol);
        var source = GenerateClass(context, containingClass, namespc, containingClassGroup.ToList(), !hasNotifyImplementtion);
        context.AddSource($ "{containingClass.Name}_AutoNotify.generated", SourceText.From(source, Encoding.UTF8));
    }
}

The `GenerateClass` method is where the entire source code is created as a string.

private string GenerateClass(GeneratorExecutionContext context, INamedTypeSymbol @class, INamespaceSymbol @namespace, List < IFieldSymbol > fields, bool implementNotifyPropertyChange) {
    var classBuilder = new StringBuilder();
    classBuilder.AppendLine("using System;");
    if (implementNotifyPropertyChange) {
        // If need to implement INotifyPropertyChange, ensure we have the required using statements
        var notifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
        var callerMemberSymbol = context.Compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.CallerMemberNameAttribute");
        classBuilder.AppendLine($ "using {notifyPropertyChangedSymbol.ContainingNamespace};");
        classBuilder.AppendLine($ "using {callerMemberSymbol.ContainingNamespace};");
        classBuilder.AppendLine($ "namespace {@namespace.ToDisplayString()}");
        classBuilder.AppendLine("{");
        classBuilder.AppendLine($ "public partial class {@class.Name}:{notifyPropertyChangedSymbol.Name}");
        classBuilder.AppendLine("{");
        classBuilder.AppendLine(GenerateNotifyPropertyChangeImplementation());
    } else {
        classBuilder.AppendLine($ "namespace {@namespace.ToDisplayString()}");
        classBuilder.AppendLine("{");
        classBuilder.AppendLine($ "public partial class {@class.Name}");
        classBuilder.AppendLine("{");
    }
    // Iterate over the fields and create the properties
    foreach(var field in fields) {
        var fieldName = field.Name;
        var fieldType = field.Type.Name;
        classBuilder.AppendLine($ "public {fieldType} {NormalizePropertyName(fieldName)}");
        classBuilder.AppendLine("{");
        classBuilder.AppendLine($ "get=> {fieldName};");
        classBuilder.AppendLine($ "set");
        classBuilder.AppendLine("{");
        classBuilder.AppendLine($ "if({fieldName} == value) return;");
        classBuilder.AppendLine($ "{fieldName} = value;");
        classBuilder.AppendLine($ "NotifyPropertyChanged();");
        classBuilder.AppendLine("}");
        classBuilder.AppendLine("}");
    }
    classBuilder.AppendLine("}");
    classBuilder.AppendLine("}");
    return classBuilder.ToString();
}

The `NormalizePropertyName` and `GenerateNotifyPropertyChangeImplementation` are as follows.

private string NormalizePropertyName(string fieldName) {
    return Regex.Replace(fieldName, "_[a-z]", delegate(Match m) {
        return m.ToString().TrimStart('_').ToUpper();
    });
}
private string GenerateNotifyPropertyChangeImplementation() {
    return @ "
    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged([CallerMemberName] string propertyName = ""
        "") {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    ";
}

That's all we need to build our Source Generator for automatically creating the implementation of `INotifyPropertyChange`. The entire source is given below.

[Generator]
public class AutoNotifyPropertyGenerator: ISourceGenerator {
    public void Execute(GeneratorExecutionContext context) {
        if (context.SyntaxContextReceiver is not FieldSyntaxReciever syntaxReciever) return;
        var sourceBuilder = new StringBuilder();
        var notifySymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
        foreach(var containingClassGroup in syntaxReciever.IdentifiedFields.GroupBy(x => x.ContainingType)) {
            var containingClass = containingClassGroup.Key;
            var namespc = containingClass.ContainingNamespace;
            var hasNotifyImplementtion = containingClass.Interfaces.Contains(notifySymbol);
            var source = GenerateClass(context, containingClass, namespc, containingClassGroup.ToList(), !hasNotifyImplementtion);
            context.AddSource($ "{containingClass.Name}_AutoNotify.generated", SourceText.From(source, Encoding.UTF8));
        }
    }
    private string GenerateClass(GeneratorExecutionContext context, INamedTypeSymbol @class, INamespaceSymbol @namespace, List < IFieldSymbol > fields, bool implementNotifyPropertyChange) {
        var classBuilder = new StringBuilder();
        classBuilder.AppendLine("using System;");
        if (implementNotifyPropertyChange) {
            var notifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
            var callerMemberSymbol = context.Compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.CallerMemberNameAttribute");
            classBuilder.AppendLine($ "using {notifyPropertyChangedSymbol.ContainingNamespace};");
            classBuilder.AppendLine($ "using {callerMemberSymbol.ContainingNamespace};");
            classBuilder.AppendLine($ "namespace {@namespace.ToDisplayString()}");
            classBuilder.AppendLine("{");
            classBuilder.AppendLine($ "public partial class {@class.Name}:{notifyPropertyChangedSymbol.Name}");
            classBuilder.AppendLine("{");
            classBuilder.AppendLine(GenerateNotifyPropertyChangeImplementation());
        } else {
            classBuilder.AppendLine($ "namespace {@namespace.ToDisplayString()}");
            classBuilder.AppendLine("{");
            classBuilder.AppendLine($ "public partial class {@class.Name}");
            classBuilder.AppendLine("{");
        }
        foreach(var field in fields) {
            var fieldName = field.Name;
            var fieldType = field.Type.Name;
            classBuilder.AppendLine($ "public {fieldType} {NormalizePropertyName(fieldName)}");
            classBuilder.AppendLine("{");
            classBuilder.AppendLine($ "get=> {fieldName};");
            classBuilder.AppendLine($ "set");
            classBuilder.AppendLine("{");
            classBuilder.AppendLine($ "if({fieldName} == value) return;");
            classBuilder.AppendLine($ "{fieldName} = value;");
            classBuilder.AppendLine($ "NotifyPropertyChanged();");
            classBuilder.AppendLine("}");
            classBuilder.AppendLine("}");
        }
        classBuilder.AppendLine("}");
        classBuilder.AppendLine("}");
        return classBuilder.ToString();
    }
    private string NormalizePropertyName(string fieldName) {
        return Regex.Replace(fieldName, "_[a-z]", delegate(Match m) {
            return m.ToString().TrimStart('_').ToUpper();
        });
    }
    private string GenerateNotifyPropertyChangeImplementation() {
        return @ "
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged([CallerMemberName] string propertyName = ""
            "") {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        ";
    }
    public void Initialize(GeneratorInitializationContext context) {
        context.RegisterForSyntaxNotifications(() => new FieldSyntaxReciever());
    }
    private class FieldSyntaxReciever: ISyntaxContextReceiver //where TAttribute:Attribute
    {
        public List < IFieldSymbol > IdentifiedFields {
            get;
        } = new List < IFieldSymbol > ();
        public void OnVisitSyntaxNode(GeneratorSyntaxContext context) {
            if (context.Node is FieldDeclarationSyntax fieldDeclaration && fieldDeclaration.AttributeLists.Any()) {
                var variableDeclaration = fieldDeclaration.Declaration.Variables;
                foreach(var variable in variableDeclaration) {
                    var field = context.SemanticModel.GetDeclaredSymbol(variable);
                    if (field is IFieldSymbol fieldInfo && fieldInfo.GetAttributes().Any(x => x.AttributeClass.ToDisplayString() == "InGen.Types.Attributes.AutoNotifyAttribute")) {
                        IdentifiedFields.Add(fieldInfo);
                    }
                }
            }
        }
    }
}

This would enable the developer to completely ignore the implementation of INotifyPropertyChanged and subsequent invocation of NotifyPropertyChanged from the properties, leaving it to the Generator to create for them. This would result in a cleaner code base with less boilerplate code but at the cost of splitting up the code into partial classes.


Similar Articles