Open sandboxFocusImprove this doc

Change Tracking example, step 4: reverting changes

In this article, we will implement the ability to revert the object to the last-accepted version. The .NET Framework exposes this ability as the IRevertibleChangeTracking interface. It adds a new RejectChanges method. This method must revert any changes performed since the last call to the AcceptChanges method.

We need to duplicate each field or automatic property: one copy will contain the current value, and the second will contain the accepted value. The AcceptChanges method copies the current values to the accepted ones, while the RejectChanges method copies the accepted values to the current ones.

Let's see this pattern in action:

Source Code
1#pragma warning disable CS8618
2



3[TrackChanges]
4[NotifyPropertyChanged]
5public partial class Comment
6{

7    public Comment( Guid id, string author, string content )
8    {
9        this.Id = id;
10        this.Author = author;
11        this.Content = content;
12    }
13
14    public Guid Id { get; }









15
16    public string Author { get; set; }




















17
18    public string Content { get; set; }




























19}
Transformed Code
1#pragma warning disable CS8618
2
3using System;
4using System.ComponentModel;
5
6[TrackChanges]
7[NotifyPropertyChanged]
8public partial class Comment
9: INotifyPropertyChanged, ISwitchableChangeTracking, IRevertibleChangeTracking, IChangeTracking
10{
11    public Comment( Guid id, string author, string content )
12    {
13        this.Id = id;
14        this.Author = author;
15        this.Content = content;
16    }
17
18    public Guid Id { get; }
19private string _author = default!;
20
21    public string Author { get { return this._author; } set { if (value != this._author) { this._author = value; OnPropertyChanged("Author"); } return; } }
22    private string _content = default!;
23
24    public string Content { get { return this._content; } set { if (value != this._content) { this._content = value; OnPropertyChanged("Content"); } return; } }
25    private string _acceptedAuthor;
26    private string _acceptedContent;
27    private bool _isTrackingChanges;
28
29    public bool IsChanged { get; private set; }
30
31    public bool IsTrackingChanges
32    {
33        get
34        {
35            return _isTrackingChanges;
36        }
37        set
38        {
39            if (this._isTrackingChanges != value)
40            {
41                this._isTrackingChanges = value;
42                this.OnPropertyChanged("IsTrackingChanges");
43                if (value)
44                {
45                    AcceptChanges();
46                }
47            }
48        }
49    }
50
51    public virtual void AcceptChanges()
52    {
53        IsChanged = false;
54        this._acceptedAuthor = this.Author;
55        this._acceptedContent = this.Content;
56    }
57    protected void OnChange()
58    {
59        if (IsChanged == false)
60        {
61            IsChanged = true;
62            this.OnPropertyChanged("IsChanged");
63        }
64    }
65    protected virtual void OnPropertyChanged(string name)
66    {
67        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
68        if (name is not ("IsChanged" or "IsTrackingChanges"))
69        {
70            OnChange();
71        }
72    }
73    public virtual void RejectChanges()
74    {
75        IsChanged = false;
76        this.Author = this._acceptedAuthor;
77        this.Content = this._acceptedContent;
78    }
79    public event PropertyChangedEventHandler? PropertyChanged;
80}
Source Code


1[TrackChanges]
2public class ModeratedComment : Comment
3{
4    public ModeratedComment( Guid id, string author, string content ) : base( id, author, content ) { }

5
6    public bool? IsApproved { get; set; }

7}
Transformed Code
1using System;
2
3[TrackChanges]
4public class ModeratedComment : Comment
5{
6    public ModeratedComment( Guid id, string author, string content ) : base( id, author, content ) { }
7private bool? _isApproved;
8
9    public bool? IsApproved { get { return this._isApproved; } set { if (value != this._isApproved) { this._isApproved = value; OnPropertyChanged("IsApproved"); } return; } }
10    private bool? _acceptedIsApproved;
11}

Aspect implementation

Here is the complete code of the new version of the TrackChanges aspect:

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using System.Globalization;
6
7[Inheritable]
8public class TrackChangesAttribute : TypeAspect
9{
10    private static readonly DiagnosticDefinition<INamedType> _mustHaveOnChangeMethod = new(
11        "MY001",
12        Severity.Error,
13        $"The '{nameof(ISwitchableChangeTracking)}' interface is implemented manually on type '{{0}}', but the type does not have an '{nameof(OnChange)}()' method." );
14
15    private static readonly DiagnosticDefinition _onChangeMethodMustBeProtected = new(
16        "MY002",
17        Severity.Error,
18        $"The '{nameof(OnChange)}()' method must be have the 'protected' accessibility." );
19
20    private static readonly DiagnosticDefinition<IMethod> _onPropertyChangedMustBeVirtual = new(
21        "MY003",
22        Severity.Error,
23        "The '{0}' method must be virtual." );
24
25    [InterfaceMember( WhenExists = InterfaceMemberOverrideStrategy.Ignore )]
26    public bool IsChanged { get; private set; }
27
28    [InterfaceMember( WhenExists = InterfaceMemberOverrideStrategy.Ignore )]
29    public bool IsTrackingChanges
30    {
31        get => meta.This._isTrackingChanges;
32        set
33        {
34            if ( meta.This._isTrackingChanges != value )
35            {
36                meta.This._isTrackingChanges = value;
37
38                var onPropertyChanged = this.GetOnPropertyChangedMethod( meta.Target.Type );
39
40                if ( onPropertyChanged != null )
41                {
42                    onPropertyChanged.Invoke( nameof(this.IsTrackingChanges) );
43                }
44
45                if ( value )
46                {
47                    this.AcceptChanges();
48                }
49            }
50        }
51    }
52
53    public override void BuildAspect( IAspectBuilder<INamedType> builder )
54    {
55        // Select fields and automatic properties that can be changed.
56        var fieldsOrProperties = builder.Target.FieldsAndProperties
57            .Where(
58                f =>
59                    !f.IsImplicitlyDeclared && f.Writeability == Writeability.All &&
60                    f.IsAutoPropertyOrField == true )
61            .ToArray();
62
63        // 
64        var introducedFields = new Dictionary<IFieldOrProperty, IField>();
65
66        // Create a field for each mutable field or property. These fields
67        // will contain the accepted values.
68        foreach ( var fieldOrProperty in fieldsOrProperties )
69        {
70            var upperCaseName = fieldOrProperty.Name.TrimStart( '_' );
71
72            upperCaseName =
73                upperCaseName.Substring( 0, 1 ).ToUpper( CultureInfo.InvariantCulture ) +
74                upperCaseName.Substring( 1 );
75
76            var acceptedField =
77                builder.Advice.IntroduceField(
78                    builder.Target,
79                    "_accepted" + upperCaseName,
80                    fieldOrProperty.Type );
81
82            introducedFields[fieldOrProperty] = acceptedField.Declaration;
83        }
84
85        // Implement the ISwitchableChangeTracking interface.         
86        var implementInterfaceResult = builder.Advice.ImplementInterface(
87            builder.Target,
88            typeof(ISwitchableChangeTracking),
89            OverrideStrategy.Ignore,
90            new { IntroducedFields = introducedFields } );
91
92        // 
93
94        // If the type already implements ISwitchableChangeTracking, it must have a protected method called OnChanged, without parameters, otherwise
95        // this is a contract violation, so we report an error.
96        if ( implementInterfaceResult.Outcome == AdviceOutcome.Ignore )
97        {
98            var onChangeMethod = builder.Target.AllMethods.OfName( nameof(this.OnChange) )
99                .SingleOrDefault( m => m.Parameters.Count == 0 );
100
101            if ( onChangeMethod == null )
102            {
103                builder.Diagnostics.Report( _mustHaveOnChangeMethod.WithArguments( builder.Target ) );
104            }
105            else if ( onChangeMethod.Accessibility != Accessibility.Protected )
106            {
107                builder.Diagnostics.Report( _onChangeMethodMustBeProtected );
108            }
109        }
110        else
111        {
112            builder.Advice.IntroduceField( builder.Target, "_isTrackingChanges", typeof(bool) );
113        }
114
115        // Override all writable fields and automatic properties.
116        // If the type has an OnPropertyChanged method, we assume that all properties
117        // and fields already call it, and we hook into OnPropertyChanged instead of
118        // overriding each setter.
119        var onPropertyChanged = this.GetOnPropertyChangedMethod( builder.Target );
120
121        if ( onPropertyChanged == null )
122        {
123            // If the type has an OnPropertyChanged method, we assume that all properties
124            // and fields already call it, and we hook into OnPropertyChanged instead of
125            // overriding each setter.
126            foreach ( var fieldOrProperty in fieldsOrProperties )
127            {
128                builder.Advice.OverrideAccessors(
129                    fieldOrProperty,
130                    null,
131                    nameof(this.OverrideSetter) );
132            }
133        }
134        else if ( onPropertyChanged.DeclaringType.Equals( builder.Target ) )
135        {
136            builder.Advice.Override( onPropertyChanged, nameof(this.OnPropertyChanged) );
137        }
138        else if ( implementInterfaceResult.Outcome == AdviceOutcome.Ignore )
139        {
140            // If we have an OnPropertyChanged method but the type already implements ISwitchableChangeTracking,
141            // we assume that the type already hooked the OnPropertyChanged method, and
142            // there is nothing else to do.
143        }
144        else
145        {
146            // If the OnPropertyChanged method was defined in a base class, but not overridden
147            // in the current class, and if we implement ISwitchableChangeTracking ourselves,
148            // then we need to override OnPropertyChanged.
149
150            if ( !onPropertyChanged.IsVirtual )
151            {
152                builder.Diagnostics.Report( _onPropertyChangedMustBeVirtual.WithArguments( onPropertyChanged ) );
153            }
154            else
155            {
156                builder.Advice.IntroduceMethod(
157                    builder.Target,
158                    nameof(this.OnPropertyChanged),
159                    whenExists: OverrideStrategy.Override );
160            }
161        }
162    }
163
164    private IMethod? GetOnPropertyChangedMethod( INamedType type )
165        => type.AllMethods
166            .OfName( "OnPropertyChanged" )
167            .SingleOrDefault( m => m.Parameters.Count == 1 );
168
169    [InterfaceMember]
170    public virtual void AcceptChanges()
171    {
172        if ( meta.Target.Method.IsOverride )
173        {
174            meta.Proceed();
175        }
176        else
177        {
178            this.IsChanged = false;
179        }
180
181        var introducedFields =
182            (Dictionary<IFieldOrProperty, IField>) meta.Tags["IntroducedFields"]!;
183
184        foreach ( var field in introducedFields )
185        {
186            field.Value.Value = field.Key.Value;
187        }
188    }
189
190    [InterfaceMember]
191    public virtual void RejectChanges()
192    {
193        if ( meta.Target.Method.IsOverride )
194        {
195            meta.Proceed();
196        }
197        else
198        {
199            this.IsChanged = false;
200        }
201
202        var introducedFields =
203            (Dictionary<IFieldOrProperty, IField>) meta.Tags["IntroducedFields"]!;
204
205        foreach ( var field in introducedFields )
206        {
207            field.Key.Value = field.Value.Value;
208        }
209    }
210
211    [Introduce( WhenExists = OverrideStrategy.Ignore )]
212    protected void OnChange()
213    {
214        if ( this.IsChanged == false )
215        {
216            this.IsChanged = true;
217
218            var onPropertyChanged = this.GetOnPropertyChangedMethod( meta.Target.Type );
219
220            if ( onPropertyChanged != null )
221            {
222                onPropertyChanged.Invoke( nameof(this.IsChanged) );
223            }
224        }
225    }
226
227    [Template]
228    private void OverrideSetter( dynamic? value )
229    {
230        meta.Proceed();
231
232        if ( value != meta.Target.Property.Value )
233        {
234            this.OnChange();
235        }
236    }
237
238    [Template]
239    protected virtual void OnPropertyChanged( string name )
240    {
241        meta.Proceed();
242
243        if ( name is not (nameof(this.IsChanged) or nameof(this.IsTrackingChanges)) )
244        {
245            this.OnChange();
246        }
247    }
248}

Let's focus on the following part of the BuildAspect method for the moment.

64var introducedFields = new Dictionary<IFieldOrProperty, IField>();
65
66// Create a field for each mutable field or property. These fields
67// will contain the accepted values.
68foreach ( var fieldOrProperty in fieldsOrProperties )
69{
70    var upperCaseName = fieldOrProperty.Name.TrimStart( '_' );
71
72    upperCaseName =
73        upperCaseName.Substring( 0, 1 ).ToUpper( CultureInfo.InvariantCulture ) +
74        upperCaseName.Substring( 1 );
75
76    var acceptedField =
77        builder.Advice.IntroduceField(
78            builder.Target,
79            "_accepted" + upperCaseName,
80            fieldOrProperty.Type );
81
82    introducedFields[fieldOrProperty] = acceptedField.Declaration;
83}
84
85// Implement the ISwitchableChangeTracking interface.         
86var implementInterfaceResult = builder.Advice.ImplementInterface(
87    builder.Target,
88    typeof(ISwitchableChangeTracking),
89    OverrideStrategy.Ignore,
90    new { IntroducedFields = introducedFields } );
91

First, the method introduces new fields into the type for each mutable field or automatic property using the IntroduceField method. For details about this practice, see Introducing members.

Note that we are building the introducedFields dictionary, which maps the current-value field or property to the accepted-value field. This dictionary will be passed to the ImplementInterface call as a tag. The collection of tags is an anonymous object. For more details about this technique, see Sharing state with advice.

The field dictionary is read from the implementation of AcceptChanges and RejectChanges:

169[InterfaceMember]
170public virtual void AcceptChanges()
171{
172    if ( meta.Target.Method.IsOverride )
173    {
174        meta.Proceed();
175    }
176    else
177    {
178        this.IsChanged = false;
179    }
180
181    var introducedFields =
182        (Dictionary<IFieldOrProperty, IField>) meta.Tags["IntroducedFields"]!;
183
184    foreach ( var field in introducedFields )
185    {
186        field.Value.Value = field.Key.Value;
187    }
188}
189

190[InterfaceMember]
191public virtual void RejectChanges()
192{
193    if ( meta.Target.Method.IsOverride )
194    {
195        meta.Proceed();
196    }
197    else
198    {
199        this.IsChanged = false;
200    }
201
202    var introducedFields =
203        (Dictionary<IFieldOrProperty, IField>) meta.Tags["IntroducedFields"]!;
204
205    foreach ( var field in introducedFields )
206    {
207        field.Key.Value = field.Value.Value;
208    }
209}
210

As you can see, the (Dictionary<IFieldOrProperty, IField>) meta.Tags["IntroducedFields"] expression gets the IntroducedFields tag, which was passed to the ImplementInterface method. We cast it back to its original type and iterate it. We use the Value property to generate the run-time expression that represents the field or property. In the AcceptChanges method, we copy the current values to the accepted ones and do the opposite in the RejectChanges method.