Skip to content

Commit 3f8c164

Browse files
authored
Merge pull request #79 from AArnott/fix78
Add INotifyPropertyChanged to Builder classes
2 parents 0ab14fa + 5ab8374 commit 3f8c164

3 files changed

Lines changed: 129 additions & 6 deletions

File tree

src/ImmutableObjectGraph.Generation.Tests/TestSources/FileSystem.Tests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,45 @@ public void RootedStruct_EqualityOperators()
572572
Assert.True(r1a != r1Rerooted);
573573
}
574574

575+
[Fact]
576+
public void Builder_INotifyPropertyChanged_SimpleValue()
577+
{
578+
var builder = FileSystemFile.CreateBuilder();
579+
var raisedName = new Stack<string>();
580+
builder.PropertyChanged += (s, e) => raisedName.Push(e.PropertyName);
581+
582+
// Set a value property and assert it raises the changed event once.
583+
builder.PathSegment = "fo";
584+
Assert.Equal(nameof(builder.PathSegment), raisedName.Pop());
585+
Assert.Equal(0, raisedName.Count);
586+
587+
// Set it to the same value and assert no event is raised.
588+
builder.PathSegment = builder.PathSegment;
589+
Assert.Equal(0, raisedName.Count);
590+
}
591+
592+
[Fact]
593+
public void Builder_INotifyPropertyChanged_LazyImmutable()
594+
{
595+
var builder = FileSystemFile.CreateBuilder();
596+
var raisedName = new Stack<string>();
597+
builder.PropertyChanged += (s, e) => raisedName.Push(e.PropertyName);
598+
599+
// Set a Builder property and assert it raises the changed event once.
600+
builder.Data = RichData.CreateBuilder();
601+
Assert.Equal(nameof(builder.Data), raisedName.Pop());
602+
Assert.Equal(0, raisedName.Count);
603+
604+
// Set it to the same value and assert no event is raised.
605+
builder.Data = builder.Data;
606+
Assert.Equal(0, raisedName.Count);
607+
608+
// Set it to another value.
609+
builder.Data = RichData.CreateBuilder();
610+
Assert.Equal(nameof(builder.Data), raisedName.Pop());
611+
Assert.Equal(0, raisedName.Count);
612+
}
613+
575614
private static void VerifyDescendentsShareRoot(RootedFileSystemDirectory directory)
576615
{
577616
foreach (var child in directory)

src/ImmutableObjectGraph.Generation.Tests/TestSources/ImmutableWithComplexStructField.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
using System;
44

5-
[GenerateImmutable]
5+
[GenerateImmutable(GenerateBuilder = true)]
66
partial class ImmutableWithComplexStructField
77
{
88
readonly SomeStructWithMultipleFields someStructField;

src/ImmutableObjectGraph.Generation/CodeGen+BuilderGen.cs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ protected class BuilderGen : FeatureGenerator
2626
private static readonly IdentifierNameSyntax ToImmutableMethodName = SyntaxFactory.IdentifierName("ToImmutable");
2727
private static readonly IdentifierNameSyntax CreateBuilderMethodName = SyntaxFactory.IdentifierName("CreateBuilder");
2828
private static readonly IdentifierNameSyntax ImmutableFieldName = SyntaxFactory.IdentifierName("immutable");
29+
private static readonly TypeSyntax INotifyPropertyChanged = SyntaxFactory.ParseTypeName("System.ComponentModel.INotifyPropertyChanged");
30+
private static readonly IdentifierNameSyntax OnPropertyChangedMethodName = SyntaxFactory.IdentifierName("OnPropertyChanged");
2931

3032
public BuilderGen(CodeGen generator)
3133
: base(generator)
@@ -55,8 +57,7 @@ protected override void GenerateCore()
5557
var builderType = SyntaxFactory.ClassDeclaration(BuilderTypeName.Identifier)
5658
.AddModifiers(
5759
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
58-
SyntaxFactory.Token(SyntaxKind.PartialKeyword))
59-
.WithMembers(SyntaxFactory.List(builderMembers));
60+
SyntaxFactory.Token(SyntaxKind.PartialKeyword));
6061
if (this.generator.applyToMetaType.HasAncestor)
6162
{
6263
builderType = builderType
@@ -66,6 +67,16 @@ protected override void GenerateCore()
6667
BuilderTypeName)))))
6768
.WithModifiers(builderType.Modifiers.Insert(0, SyntaxFactory.Token(SyntaxKind.NewKeyword)));
6869
}
70+
else
71+
{
72+
builderType = builderType
73+
.AddBaseListTypes(SyntaxFactory.SimpleBaseType(INotifyPropertyChanged));
74+
builderMembers.Add(this.CreatePropertyChangedEvent());
75+
builderMembers.Add(this.CreateOnPropertyChangedMethod());
76+
}
77+
78+
builderType = builderType
79+
.WithMembers(SyntaxFactory.List(builderMembers));
6980

7081
this.innerMembers.Add(builderType);
7182
}
@@ -182,11 +193,12 @@ protected IReadOnlyList<MemberDeclarationSyntax> CreateMutableProperties()
182193
foreach (var field in this.generator.applyToMetaType.LocalFields)
183194
{
184195
var thisField = Syntax.ThisDot(field.NameAsField);
196+
var optionalFieldNotYetDefined = SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, Syntax.OptionalIsDefined(thisField));
185197
var getterBlock = field.IsGeneratedImmutableType
186198
? SyntaxFactory.Block(
187199
// if (!this.fieldName.IsDefined) {
188200
SyntaxFactory.IfStatement(
189-
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, Syntax.OptionalIsDefined(thisField)),
201+
optionalFieldNotYetDefined,
190202
SyntaxFactory.Block(
191203
// this.fieldName = this.immutable.fieldName?.ToBuilder();
192204
SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression(
@@ -202,11 +214,38 @@ protected IReadOnlyList<MemberDeclarationSyntax> CreateMutableProperties()
202214
SyntaxFactory.ArgumentList())))))),
203215
SyntaxFactory.ReturnStatement(Syntax.OptionalValue(thisField)))
204216
: SyntaxFactory.Block(SyntaxFactory.ReturnStatement(thisField));
205-
var setterBlock = SyntaxFactory.Block(
217+
var setterValueArg = SyntaxFactory.IdentifierName("value");
218+
var setterCondition = field.IsGeneratedImmutableType ?
219+
SyntaxFactory.BinaryExpression(
220+
SyntaxKind.LogicalOrExpression,
221+
optionalFieldNotYetDefined,
222+
SyntaxFactory.BinaryExpression(
223+
SyntaxKind.NotEqualsExpression,
224+
Syntax.OptionalValue(thisField),
225+
setterValueArg)) :
226+
HasEqualityOperators(field.Symbol.Type) ?
227+
SyntaxFactory.BinaryExpression(
228+
SyntaxKind.NotEqualsExpression,
229+
thisField,
230+
setterValueArg) :
231+
null;
232+
var setterSignificantBlock = SyntaxFactory.Block(
206233
SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression(
207234
SyntaxKind.SimpleAssignmentExpression,
208235
thisField,
209-
SyntaxFactory.IdentifierName("value"))));
236+
setterValueArg)),
237+
SyntaxFactory.ExpressionStatement(
238+
SyntaxFactory.InvocationExpression(
239+
SyntaxFactory.MemberAccessExpression(
240+
SyntaxKind.SimpleMemberAccessExpression,
241+
SyntaxFactory.ThisExpression(),
242+
OnPropertyChangedMethodName))));
243+
var setterBlock = setterCondition != null ?
244+
SyntaxFactory.Block(
245+
SyntaxFactory.IfStatement(
246+
setterCondition,
247+
setterSignificantBlock)) :
248+
setterSignificantBlock;
210249

211250
var property = SyntaxFactory.PropertyDeclaration(
212251
this.GetPropertyTypeForBuilder(field),
@@ -239,6 +278,51 @@ protected NameSyntax GetFieldTypeForBuilder(MetaField field)
239278
: typeBasis;
240279
}
241280

281+
protected EventFieldDeclarationSyntax CreatePropertyChangedEvent()
282+
{
283+
var handler = SyntaxFactory.ParseTypeName("System.ComponentModel.PropertyChangedEventHandler");
284+
return SyntaxFactory.EventFieldDeclaration(
285+
SyntaxFactory.VariableDeclaration(handler)
286+
.AddVariables(SyntaxFactory.VariableDeclarator(nameof(System.ComponentModel.INotifyPropertyChanged.PropertyChanged))))
287+
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)));
288+
}
289+
290+
protected MethodDeclarationSyntax CreateOnPropertyChangedMethod()
291+
{
292+
var callerMemberName = SyntaxFactory.ParseName("System.Runtime.CompilerServices.CallerMemberNameAttribute");
293+
var propertyNameParameterName = SyntaxFactory.IdentifierName("propertyName");
294+
var evt = SyntaxFactory.MemberAccessExpression(
295+
SyntaxKind.SimpleMemberAccessExpression,
296+
SyntaxFactory.ThisExpression(),
297+
SyntaxFactory.IdentifierName(nameof(System.ComponentModel.INotifyPropertyChanged.PropertyChanged)));
298+
var invokeMethod = SyntaxFactory.ConditionalAccessExpression(
299+
evt,
300+
SyntaxFactory.InvocationExpression(
301+
SyntaxFactory.MemberBindingExpression(SyntaxFactory.IdentifierName(nameof(System.ComponentModel.PropertyChangedEventHandler.Invoke))),
302+
SyntaxFactory.ArgumentList().AddArguments(
303+
SyntaxFactory.Argument(SyntaxFactory.ThisExpression()),
304+
SyntaxFactory.Argument(
305+
SyntaxFactory.ObjectCreationExpression(
306+
SyntaxFactory.ParseTypeName("System.ComponentModel.PropertyChangedEventArgs"),
307+
SyntaxFactory.ArgumentList().AddArguments(
308+
SyntaxFactory.Argument(propertyNameParameterName)),
309+
null)))));
310+
var body = SyntaxFactory.Block(
311+
SyntaxFactory.ExpressionStatement(invokeMethod));
312+
return SyntaxFactory.MethodDeclaration(
313+
SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
314+
OnPropertyChangedMethodName.Identifier)
315+
.AddParameterListParameters(
316+
SyntaxFactory.Parameter(propertyNameParameterName.Identifier)
317+
.WithType(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)))
318+
.AddAttributeLists(SyntaxFactory.AttributeList().AddAttributes(SyntaxFactory.Attribute(callerMemberName)))
319+
.WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(string.Empty)))))
320+
.AddModifiers(
321+
SyntaxFactory.Token(SyntaxKind.ProtectedKeyword),
322+
SyntaxFactory.Token(SyntaxKind.VirtualKeyword))
323+
.WithBody(body);
324+
}
325+
242326
protected MethodDeclarationSyntax CreateToImmutableMethod()
243327
{
244328
// var fieldName = this.fieldName.IsDefined ? this.fieldName.Value?.ToImmutable() : this.immutable.FieldName;

0 commit comments

Comments
 (0)