In the previous article, we created an aspect that automatically implements
the IChangeTracking interface. If the base class has a manual implementation of
the IChangeTracking, the aspect will still work correctly and call the OnChange
method of
the base class. However, what if the base class does not contain an OnChange
method or if it is not protected? Let's
improve the aspect and report an error in these situations.
The result of this aspect will be two new errors:
1namespace Metalama.Samples.Clone.Tests.MissingOnChangeMethod;
2
3[TrackChanges]
Error MY001: The 'ISwitchableChangeTracking' interface is implemented manually on type 'DerivedClass', but the type does not have an 'OnChange()' method.
4public class DerivedClass : BaseClass
5{
6}
7
8public class BaseClass : ISwitchableChangeTracking
9{
10 public bool IsChanged { get; protected set; }
11
12 public bool IsTrackingChanges { get; set; }
13
14 public void AcceptChanges()
15 {
16 if (this.IsTrackingChanges)
17 {
18 this.IsChanged = false;
19 }
20 }
21
22 // Note that there is NO OnChange method.
23}
1namespace Metalama.Samples.Clone.Tests.OnChangeMethodNotProtected;
2
3[TrackChanges]
Error MY001: The 'ISwitchableChangeTracking' interface is implemented manually on type 'DerivedClass', but the type does not have an 'OnChange()' method.
4public class DerivedClass : BaseClass
5{
6}
7
8public class BaseClass : ISwitchableChangeTracking
9{
10 public bool IsChanged { get; protected set; }
11
12 public bool IsTrackingChanges { get; set; }
13
14 public void AcceptChanges()
15 {
16 if (this.IsTrackingChanges)
17 {
18 this.IsChanged = false;
19 }
20 }
21
22
23 // Note that the OnChange method is private and not protected.
24 private void OnChange() => this.IsChanged = true;
25}
Aspect implementation
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5
6
7public class TrackChangesAttribute : TypeAspect
8{
9 private static readonly DiagnosticDefinition<INamedType> _mustHaveOnChangeMethod = new(
10 "MY001",
11 Severity.Error,
12 $"The '{nameof(ISwitchableChangeTracking)}' interface is implemented manually on type '{{0}}', but the type does not have an '{nameof(OnChange)}()' method.");
13
14 private static readonly DiagnosticDefinition _onChangeMethodMustBeProtected = new(
15 "MY002",
16 Severity.Error,
17 $"The '{nameof(OnChange)}()' method must be have the 'protected' accessibility.");
18
19 public override void BuildAspect(IAspectBuilder<INamedType> builder)
20 {
21 //
22 // Implement the ISwitchableChangeTracking interface.
23 var implementInterfaceResult = builder.Advice.ImplementInterface(builder.Target,
24 typeof(ISwitchableChangeTracking), OverrideStrategy.Ignore);
25
26 // If the type already implements IChangeTracking, it must have a protected method called OnChanged, without parameters, otherwise
27 // this is a contract violation, so we report an error.
28 if (implementInterfaceResult.Outcome == AdviceOutcome.Ignore)
29 {
30 var onChangeMethod = builder.Target.AllMethods.OfName(nameof(this.OnChange))
31 .SingleOrDefault(m => m.Parameters.Count == 0);
32
33 if (onChangeMethod == null)
34 {
35 builder.Diagnostics.Report(
36 _mustHaveOnChangeMethod.WithArguments(builder.Target));
37 }
38 else if (onChangeMethod.Accessibility != Accessibility.Protected)
39 {
40 builder.Diagnostics.Report(_onChangeMethodMustBeProtected);
41 }
42 }
43 //
44
45 // Override all writable fields and automatic properties.
46 var fieldsOrProperties = builder.Target.FieldsAndProperties
47 .Where(f =>
48 !f.IsImplicitlyDeclared && f.Writeability == Writeability.All &&
49 f.IsAutoPropertyOrField == true);
50
51 foreach (var fieldOrProperty in fieldsOrProperties)
52 {
53 builder.Advice.OverrideAccessors(fieldOrProperty, null, nameof(this.OverrideSetter));
54 }
55 }
56
57
58 [InterfaceMember] public bool IsChanged { get; private set; }
59
60 [InterfaceMember] public bool IsTrackingChanges { get; set; }
61
62
63 [InterfaceMember]
64 public void AcceptChanges() => this.IsChanged = false;
65
66 [Introduce(WhenExists = OverrideStrategy.Ignore)]
67 protected void OnChange()
68 {
69 if (this.IsTrackingChanges)
70 {
71 this.IsChanged = true;
72 }
73 }
74
75 [Template]
76 private void OverrideSetter(dynamic? value)
77 {
78 if (value != meta.Target.Property.Value)
79 {
80 meta.Proceed();
81
82 this.OnChange();
83 }
84 }
85}
The first thing we add to the TrackChangesAttribute
is two static fields to define the errors:
9private static readonly DiagnosticDefinition<INamedType> _mustHaveOnChangeMethod = new(
10 "MY001",
11 Severity.Error,
12 $"The '{nameof(ISwitchableChangeTracking)}' interface is implemented manually on type '{{0}}', but the type does not have an '{nameof(OnChange)}()' method.");
13
14private static readonly DiagnosticDefinition _onChangeMethodMustBeProtected = new(
15 "MY002",
16 Severity.Error,
17 $"The '{nameof(OnChange)}()' method must be have the 'protected' accessibility.");
18
Metalama requires the DiagnosticDefinition to be defined in a static field or property. To learn more about reporting errors, see Reporting and suppressing diagnostics.
Then, we add this code to the BuildAspect
method:
22// Implement the ISwitchableChangeTracking interface.
23var implementInterfaceResult = builder.Advice.ImplementInterface(builder.Target,
24 typeof(ISwitchableChangeTracking), OverrideStrategy.Ignore);
25
26// If the type already implements IChangeTracking, it must have a protected method called OnChanged, without parameters, otherwise
27// this is a contract violation, so we report an error.
28if (implementInterfaceResult.Outcome == AdviceOutcome.Ignore)
29{
30 var onChangeMethod = builder.Target.AllMethods.OfName(nameof(this.OnChange))
31 .SingleOrDefault(m => m.Parameters.Count == 0);
32
33 if (onChangeMethod == null)
34 {
35 builder.Diagnostics.Report(
36 _mustHaveOnChangeMethod.WithArguments(builder.Target));
37 }
38 else if (onChangeMethod.Accessibility != Accessibility.Protected)
39 {
40 builder.Diagnostics.Report(_onChangeMethodMustBeProtected);
41 }
42}
As in the previous step, the BuildAspect
method
calls ImplementInterface with
the Ignore
OverrideStrategy. This time, we inspect the outcome
of ImplementInterface. If the outcome is Ignored
, it means that
the
type or any base type already implements the IChangeTracking interface. In this case, we
check that the type contains a parameterless method named OnChange
and verify its accessibility.
Summary
This article explained how to report an error when the source code does not meet the expectations of the aspect. To make our aspect usable in practice, i.e., to make it possible to enable or disable a hypothetical Save button when the user performs changes in the UI, we still have to integrate with the INotifyPropertyChanged interface and raise the PropertyChanged event when the IsChanged property changes. We will see how to do this in the following article.