Skip to content

Commit c92c1a2

Browse files
author
David Wengier
committed
Add a couple more samples
1 parent 552ce82 commit c92c1a2

File tree

6 files changed

+294
-2
lines changed

6 files changed

+294
-2
lines changed

MainForm.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ public partial class MainForm : Form
1111

1212
public MainForm()
1313
{
14-
System.Console.BackgroundColor = ConsoleColor.White;
1514
this.StartPosition = FormStartPosition.WindowsDefaultBounds;
1615
this.Text = "Source Generator Playground - v" + ThisAssembly.AssemblyInformationalVersion;
1716

@@ -59,7 +58,7 @@ public MainForm()
5958
loadMenu.DropDownItems.Add(name.Split(".")[2]).Click += LoadSample;
6059
}
6160
}
62-
loadMenu.DropDownItems[0].PerformClick();
61+
loadMenu.DropDownItems[2].PerformClick();
6362
this.MainMenuStrip.Visible = true;
6463

6564
void LoadSample(object? s, EventArgs e)

Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ private static void Main()
1616
Application.SetCompatibleTextRenderingDefault(false);
1717
using var mainForm = new MainForm();
1818
Application.Run(mainForm);
19+
20+
// TODO: Use https://github.com/jaredpar/roslyn-codedom
21+
System.Console.BackgroundColor = ConsoleColor.White;
22+
System.ComponentModel.INotifyPropertyChanged? c = null;
1923
}
2024
}
2125
}

Samples/Auto Notify.Generator.cs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Text;
10+
11+
namespace SourceGeneratorSamples
12+
{
13+
[Generator]
14+
public class AutoNotifyGenerator : ISourceGenerator
15+
{
16+
private const string attributeText = @"
17+
using System;
18+
namespace AutoNotify
19+
{
20+
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
21+
sealed class AutoNotifyAttribute : Attribute
22+
{
23+
public AutoNotifyAttribute()
24+
{
25+
}
26+
public string PropertyName { get; set; }
27+
}
28+
}
29+
";
30+
31+
public void Initialize(InitializationContext context)
32+
{
33+
// Register a syntax receiver that will be created for each generation pass
34+
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
35+
}
36+
37+
public void Execute(SourceGeneratorContext context)
38+
{
39+
// add the attribute text
40+
context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));
41+
42+
// retreive the populated receiver
43+
if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
44+
return;
45+
46+
// we're going to create a new compilation that contains the attribute.
47+
// TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
48+
CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
49+
Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
50+
51+
// get the newly bound attribute, and INotifyPropertyChanged
52+
INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
53+
INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
54+
55+
// loop over the candidate fields, and keep the ones that are actually annotated
56+
List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
57+
foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
58+
{
59+
SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
60+
foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
61+
{
62+
// Get the symbol being decleared by the field, and keep it if its annotated
63+
IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
64+
if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
65+
{
66+
fieldSymbols.Add(fieldSymbol);
67+
}
68+
}
69+
}
70+
71+
// group the fields by class, and generate the source
72+
foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
73+
{
74+
string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
75+
context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
76+
}
77+
}
78+
79+
private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
80+
{
81+
if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
82+
{
83+
return null; //TODO: issue a diagnostic that it must be top level
84+
}
85+
86+
string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
87+
88+
// begin building the generated source
89+
StringBuilder source = new StringBuilder($@"
90+
namespace {namespaceName}
91+
{{
92+
public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
93+
{{
94+
");
95+
96+
// if the class doesn't implement INotifyPropertyChanged already, add it
97+
if (!classSymbol.Interfaces.Contains(notifySymbol))
98+
{
99+
source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
100+
}
101+
102+
// create properties for each field
103+
foreach (IFieldSymbol fieldSymbol in fields)
104+
{
105+
ProcessField(source, fieldSymbol, attributeSymbol);
106+
}
107+
108+
source.Append("} }");
109+
return source.ToString();
110+
}
111+
112+
private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
113+
{
114+
// get the name and type of the field
115+
string fieldName = fieldSymbol.Name;
116+
ITypeSymbol fieldType = fieldSymbol.Type;
117+
118+
// get the AutoNotify attribute from the field, and any associated data
119+
AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
120+
TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;
121+
122+
string propertyName = chooseName(fieldName, overridenNameOpt);
123+
if (propertyName.Length == 0 || propertyName == fieldName)
124+
{
125+
//TODO: issue a diagnostic that we can't process this field
126+
return;
127+
}
128+
129+
source.Append($@"
130+
public {fieldType} {propertyName}
131+
{{
132+
get
133+
{{
134+
return this.{fieldName};
135+
}}
136+
137+
set
138+
{{
139+
this.{fieldName} = value;
140+
this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
141+
}}
142+
}}
143+
144+
");
145+
146+
string chooseName(string fieldName, TypedConstant overridenNameOpt)
147+
{
148+
if (!overridenNameOpt.IsNull)
149+
{
150+
return overridenNameOpt.Value.ToString();
151+
}
152+
153+
fieldName = fieldName.TrimStart('_');
154+
if (fieldName.Length == 0)
155+
return string.Empty;
156+
157+
if (fieldName.Length == 1)
158+
return fieldName.ToUpper();
159+
160+
return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
161+
}
162+
163+
}
164+
165+
/// <summary>
166+
/// Created on demand before each generation pass
167+
/// </summary>
168+
class SyntaxReceiver : ISyntaxReceiver
169+
{
170+
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
171+
172+
/// <summary>
173+
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
174+
/// </summary>
175+
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
176+
{
177+
// any field with at least one attribute is a candidate for property generation
178+
if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
179+
&& fieldDeclarationSyntax.AttributeLists.Count > 0)
180+
{
181+
CandidateFields.Add(fieldDeclarationSyntax);
182+
}
183+
}
184+
}
185+
}
186+
}

Samples/Auto Notify.Program.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
3+
using AutoNotify;
4+
5+
namespace GeneratedDemo
6+
{
7+
// The view model we'd like to augment
8+
public partial class ExampleViewModel
9+
{
10+
[AutoNotify]
11+
private string _text = "private field text";
12+
13+
[AutoNotify(PropertyName = "Count")]
14+
private int _amount = 5;
15+
}
16+
17+
public static class Program
18+
{
19+
public static void Main()
20+
{
21+
ExampleViewModel vm = new ExampleViewModel();
22+
23+
// we didn't explicitly create the 'Text' property, it was generated for us
24+
string text = vm.Text;
25+
Console.WriteLine($"Text = {text}");
26+
27+
// Properties can have differnt names generated based on the PropertyName argument of the attribute
28+
int count = vm.Count;
29+
Console.WriteLine($"Count = {count}");
30+
31+
// the viewmodel will automatically implement INotifyPropertyChanged
32+
vm.PropertyChanged += (o, e) => Console.WriteLine($"Property {e.PropertyName} was changed");
33+
vm.Text = "abc";
34+
vm.Count = 123;
35+
36+
// Try adding fields to the ExampleViewModel class above and tagging them with the [AutoNotify] attribute
37+
// You'll see the matching generated properties visibile in IntelliSense in realtime
38+
}
39+
}
40+
}

Samples/Hello World.Generator.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Text;
7+
8+
namespace SourceGeneratorSamples
9+
{
10+
[Generator]
11+
public class HelloWorldGenerator : ISourceGenerator
12+
{
13+
public void Execute(SourceGeneratorContext context)
14+
{
15+
// begin creating the source we'll inject into the users compilation
16+
StringBuilder sourceBuilder = new StringBuilder(@"
17+
using System;
18+
namespace HelloWorldGenerated
19+
{
20+
public static class HelloWorld
21+
{
22+
public static void SayHello()
23+
{
24+
Console.WriteLine(""Hello from generated code!"");
25+
Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"");
26+
");
27+
28+
// using the context, get a list of syntax trees in the users compilation
29+
IEnumerable<SyntaxTree> syntaxTrees = context.Compilation.SyntaxTrees;
30+
31+
// add the filepath of each tree to the class we're building
32+
foreach (SyntaxTree tree in syntaxTrees)
33+
{
34+
sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");");
35+
}
36+
37+
// finish creating the source to inject
38+
sourceBuilder.Append(@"
39+
}
40+
}
41+
}");
42+
43+
// inject the created source into the users compilation
44+
context.AddSource("helloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
45+
}
46+
47+
public void Initialize(InitializationContext context)
48+
{
49+
// No initialization required
50+
}
51+
}
52+
}

Samples/Hello World.Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+

2+
namespace MyApp
3+
{
4+
class Program
5+
{
6+
static void Main()
7+
{
8+
HelloWorldGenerated.HelloWorld.SayHello();
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)