-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Unsaved changes bubble-up pattern (#18)
- Loading branch information
1 parent
9faed64
commit a253313
Showing
21 changed files
with
757 additions
and
1 deletion.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
Moyou.Aspects/Moyou.Aspects.UnsavedChanges/IUnsavedChanges.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using Metalama.Framework.Aspects; | ||
|
||
namespace Moyou.Aspects.UnsavedChanges; | ||
|
||
[RunTime] | ||
public interface IUnsavedChanges | ||
{ | ||
/// <summary> | ||
/// True if this object or any child object has unsaved changes. | ||
/// </summary> | ||
bool UnsavedChanges { get; } | ||
/// <summary> | ||
/// Resets unsaved changes in this object and all child objects that implement <see cref="IUnsavedChanges"/>. | ||
/// </summary> | ||
void ResetUnsavedChanges(); | ||
} |
17 changes: 17 additions & 0 deletions
17
Moyou.Aspects/Moyou.Aspects.UnsavedChanges/Moyou.Aspects.UnsavedChanges.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Metalama.Framework" Version="2024.2.23" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\Moyou.Extensions\Moyou.Extensions.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
198 changes: 198 additions & 0 deletions
198
Moyou.Aspects/Moyou.Aspects.UnsavedChanges/UnsavedChangesAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
using Metalama.Framework.Advising; | ||
using Metalama.Framework.Aspects; | ||
using Metalama.Framework.Code; | ||
using Metalama.Framework.Code.SyntaxBuilders; | ||
using Moyou.Extensions; | ||
|
||
namespace Moyou.Aspects.UnsavedChanges; | ||
|
||
/// <summary> | ||
/// Implements a bubbling-up unsaved changes field and a method to reset via <see cref="IUnsavedChanges"/>. | ||
/// All members in a type marked with this attribute that are either themselves marked with this attribute or are an | ||
/// IEnumerable of a type marked with this attribute will be considered for <see cref="GetUnsavedChanges"/> and | ||
/// <see cref="ResetUnsavedChanges"/>. | ||
/// </summary> | ||
/// <remarks>You must still determine yourself when the object itself has unsaved changes and set | ||
/// <see cref="_internalUnsavedChanges"/> yourself accordingly (for example, in your property setters).</remarks> | ||
/// <seealso cref="IUnsavedChanges"/> | ||
public class UnsavedChangesAttribute : TypeAspect | ||
{ | ||
public override void BuildAspect(IAspectBuilder<INamedType> builder) | ||
{ | ||
base.BuildAspect(builder); | ||
|
||
builder.ImplementInterface(typeof(IUnsavedChanges), OverrideStrategy.Ignore); | ||
|
||
builder.IntroduceField(nameof(_internalUnsavedChanges), IntroductionScope.Instance, buildField: | ||
fbuilder => { fbuilder.Accessibility = Accessibility.Private; }); | ||
|
||
//find all members whose type implements UnsavedChangesAttribute themselves (via interface) | ||
var relevantMembers = builder.Target.AllFieldsAndProperties | ||
.Where(member => member.TypeHasAttribute(typeof(UnsavedChangesAttribute))) | ||
//exclude auto backing fields | ||
.Where(member => member is not IField field || !field.IsAutoBackingField()) | ||
.ToList(); | ||
|
||
//find all members whose type is ienumerable of a type that implements UnsavedChangesAttribute (via interface) | ||
var relevantIEnumerableMembers = builder.Target.AllFieldsAndProperties | ||
.Where(member => member.Type is INamedType ntype && | ||
//strings are IEnumerable<char> so we need to exclude them | ||
!ntype.Is(SpecialType.String) && | ||
ntype.Is(typeof(IEnumerable<>), ConversionKind.TypeDefinition)) | ||
.Where(member => member.TypeArgumentOfEnumerableHasAttribute(typeof(UnsavedChangesAttribute))) | ||
.Where(member => member is not IField field || !field.IsAutoBackingField()) | ||
.ToList(); | ||
|
||
|
||
//pass all of these to the method template | ||
builder.IntroduceMethod(nameof(GetUnsavedChanges), IntroductionScope.Instance, | ||
buildMethod: mBuilder => { mBuilder.Accessibility = Accessibility.Private; }, | ||
args: new { relevantMembers, relevantIEnumerableMembers }); | ||
|
||
builder.IntroduceMethod(nameof(ResetUnsavedChanges), IntroductionScope.Instance, | ||
buildMethod: mBuilder => { mBuilder.Accessibility = Accessibility.Public; }, | ||
args: new { relevantMembers, relevantIEnumerableMembers }); | ||
|
||
builder.IntroduceProperty(nameof(UnsavedChanges), IntroductionScope.Instance, | ||
buildProperty: pBuilder => { pBuilder.Accessibility = Accessibility.Public; }); | ||
} | ||
|
||
/// <summary> | ||
/// Whether this object itself has unsaved changes (regardless of child members). | ||
/// </summary> | ||
[Template] private bool _internalUnsavedChanges = false; | ||
|
||
[Template] public bool UnsavedChanges => meta.This.GetUnsavedChanges(); | ||
|
||
[Template] | ||
private static bool GetUnsavedChanges( | ||
[CompileTime] IEnumerable<IFieldOrProperty> relevantMembers, | ||
[CompileTime] IEnumerable<IFieldOrProperty> relevantIEnumerableMembers | ||
) | ||
{ | ||
var exprBuilder = new ExpressionBuilder(); | ||
exprBuilder.AppendExpression(meta.This._internalUnsavedChanges); | ||
foreach (var member in relevantMembers) | ||
{ | ||
exprBuilder.AppendVerbatim("||"); | ||
if (member.Type.IsNullable!.Value) | ||
// ReSharper disable once ArrangeRedundantParentheses because we need the parantheses in the expression | ||
exprBuilder.AppendExpression((member.Value?.UnsavedChanges ?? false)); | ||
else | ||
exprBuilder.AppendExpression(member.Value?.UnsavedChanges); | ||
} | ||
|
||
foreach (var member in relevantIEnumerableMembers) | ||
{ | ||
exprBuilder.AppendVerbatim("||"); | ||
var enumerableNullable = meta.CompileTime(member.Type.IsNullable!.Value); | ||
var genericTypeNullable = meta.CompileTime((INamedType)member.Type).TypeArguments[0].IsNullable!.Value; | ||
GetUnsavedChangesHandleIEnumerable(enumerableNullable, genericTypeNullable, member, exprBuilder); | ||
} | ||
|
||
return exprBuilder.ToExpression().Value; | ||
} | ||
|
||
/// <summary> | ||
/// Handles code generation for IEnumerable members for <see cref="GetUnsavedChanges"/>. | ||
/// </summary> | ||
/// <param name="enumerableNullable">The IEnumerable itself is nullable, i.e. <c>IEnumerable<Foobar>?</c>.</param> | ||
/// <param name="genericTypeNullable">The type inside the IEnumerable is nullable, i.e. <c>IEnumerable<Foobar?></c>.</param> | ||
/// <param name="member">The member itself.</param> | ||
/// <param name="exprBuilder">The current expression builder.</param> | ||
[Template] | ||
private static void GetUnsavedChangesHandleIEnumerable( | ||
[CompileTime] bool enumerableNullable, | ||
[CompileTime] bool genericTypeNullable, | ||
[CompileTime] IFieldOrProperty member, | ||
[CompileTime] ExpressionBuilder exprBuilder | ||
) | ||
{ | ||
if (enumerableNullable) | ||
{ | ||
//TODO: maybe un-verbatim-ify this? | ||
exprBuilder.AppendVerbatim(genericTypeNullable | ||
? $"({member.Name} is null ? false : {member.Name}.Any(v => v?.UnsavedChanges ?? false))" | ||
: $"({member.Name} is null ? false : {member.Name}.Any(v => v.UnsavedChanges))"); | ||
} | ||
else | ||
{ | ||
exprBuilder.AppendExpression(genericTypeNullable | ||
? ((IEnumerable<IUnsavedChanges?>)member.Value!).Any(v => v?.UnsavedChanges ?? false) | ||
: ((IEnumerable<IUnsavedChanges>)member.Value!).Any(v => v.UnsavedChanges)); | ||
} | ||
} | ||
|
||
[Template] | ||
public static void ResetUnsavedChanges( | ||
[CompileTime] IEnumerable<IFieldOrProperty> relevantMembers, | ||
[CompileTime] IEnumerable<IFieldOrProperty> relevantIEnumerableMembers | ||
) | ||
{ | ||
meta.This._internalUnsavedChanges = false; | ||
foreach (var member in relevantMembers) | ||
{ | ||
member.Value?.ResetUnsavedChanges(); | ||
} | ||
|
||
foreach (var member in relevantIEnumerableMembers) | ||
{ | ||
var enumerableNullable = meta.CompileTime(member.Type.IsNullable!.Value); | ||
var genericTypeNullable = meta.CompileTime((INamedType)member.Type).TypeArguments[0].IsNullable!.Value; | ||
ResetUnsavedChangesHandleIEnumerable(enumerableNullable, genericTypeNullable, member); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Handles code generation for IEnumerable members for <see cref="ResetUnsavedChanges"/>. | ||
/// </summary> | ||
/// <param name="enumerableNullable">The IEnumerable itself is nullable, i.e. <c>IEnumerable<Foobar>?</c>.</param> | ||
/// <param name="genericTypeNullable">The type inside the IEnumerable is nullable, i.e. <c>IEnumerable<Foobar?></c>.</param> | ||
/// <param name="member">The member itself.</param> | ||
[Template] | ||
private static void ResetUnsavedChangesHandleIEnumerable( | ||
[CompileTime] bool enumerableNullable, | ||
[CompileTime] bool genericTypeNullable, | ||
[CompileTime] IFieldOrProperty member | ||
) | ||
{ | ||
if (enumerableNullable) | ||
{ | ||
// ReSharper disable once InvertIf | ||
// limitation: https://doc.postsharp.net/metalama/conceptual/aspects/templates/auxilliary-templates see note | ||
if (member.Value is not null) | ||
{ | ||
ResetUnsavedChangesHandleIEnumerableInternal(genericTypeNullable, member); | ||
} | ||
} | ||
else | ||
{ | ||
ResetUnsavedChangesHandleIEnumerableInternal(genericTypeNullable, member); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Helper method for <see cref="ResetUnsavedChangesHandleIEnumerable"/>. | ||
/// </summary> | ||
/// <param name="genericTypeNullable">The type inside the IEnumerable is nullable, i.e. <c>IEnumerable<Foobar?></c>.</param> | ||
/// <param name="member">The member itself.</param> | ||
[Template] | ||
private static void ResetUnsavedChangesHandleIEnumerableInternal([CompileTime] bool genericTypeNullable, | ||
[CompileTime] IFieldOrProperty member) | ||
{ | ||
if (genericTypeNullable) | ||
{ | ||
foreach (var val in member.Value!) | ||
{ | ||
val?.ResetUnsavedChanges(); | ||
} | ||
} | ||
else | ||
{ | ||
foreach (var val in member.Value!) | ||
{ | ||
val.ResetUnsavedChanges(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
Moyou.Test/UnsavedChanges/UnsavedChangesAttributeTest/NestedIEnumerable.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using Moyou.Aspects.UnsavedChanges; | ||
|
||
namespace Moyou.CompileTimeTest.UnsavedChanges.UnsavedChangesAttributeTest; | ||
|
||
[UnsavedChanges] | ||
public class NestedIEnumerable | ||
{ | ||
public IEnumerable<B> Bs { get; set; } | ||
public D D { get; set; } | ||
} | ||
|
||
[UnsavedChanges] | ||
public class B | ||
{ | ||
|
||
} | ||
|
||
[UnsavedChanges] | ||
public class D | ||
{ | ||
|
||
} |
69 changes: 69 additions & 0 deletions
69
Moyou.Test/UnsavedChanges/UnsavedChangesAttributeTest/NestedIEnumerable.t.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
using Moyou.Aspects.UnsavedChanges; | ||
namespace Moyou.CompileTimeTest.UnsavedChanges.UnsavedChangesAttributeTest; | ||
[UnsavedChanges] | ||
public class NestedIEnumerable : global::Moyou.Aspects.UnsavedChanges.IUnsavedChanges | ||
{ | ||
public IEnumerable<B> Bs { get; set; } | ||
public D D { get; set; } | ||
private global::System.Boolean _internalUnsavedChanges = (global::System.Boolean)false; | ||
public global::System.Boolean UnsavedChanges | ||
{ | ||
get | ||
{ | ||
return (global::System.Boolean)this.GetUnsavedChanges(); | ||
} | ||
} | ||
private global::System.Boolean GetUnsavedChanges() | ||
{ | ||
return (global::System.Boolean)(this._internalUnsavedChanges || this.D.UnsavedChanges || global::System.Linq.Enumerable.Any(((global::System.Collections.Generic.IEnumerable<global::Moyou.Aspects.UnsavedChanges.IUnsavedChanges>)this.Bs), v_1 => v_1.UnsavedChanges)); | ||
} | ||
public void ResetUnsavedChanges() | ||
{ | ||
this._internalUnsavedChanges = false; | ||
this.D.ResetUnsavedChanges(); | ||
foreach (var val in this.Bs) | ||
{ | ||
val.ResetUnsavedChanges(); | ||
} | ||
} | ||
} | ||
[UnsavedChanges] | ||
public class B : global::Moyou.Aspects.UnsavedChanges.IUnsavedChanges | ||
{ | ||
private global::System.Boolean _internalUnsavedChanges = (global::System.Boolean)false; | ||
public global::System.Boolean UnsavedChanges | ||
{ | ||
get | ||
{ | ||
return (global::System.Boolean)this.GetUnsavedChanges(); | ||
} | ||
} | ||
private global::System.Boolean GetUnsavedChanges() | ||
{ | ||
return (global::System.Boolean)this._internalUnsavedChanges; | ||
} | ||
public void ResetUnsavedChanges() | ||
{ | ||
this._internalUnsavedChanges = false; | ||
} | ||
} | ||
[UnsavedChanges] | ||
public class D : global::Moyou.Aspects.UnsavedChanges.IUnsavedChanges | ||
{ | ||
private global::System.Boolean _internalUnsavedChanges = (global::System.Boolean)false; | ||
public global::System.Boolean UnsavedChanges | ||
{ | ||
get | ||
{ | ||
return (global::System.Boolean)this.GetUnsavedChanges(); | ||
} | ||
} | ||
private global::System.Boolean GetUnsavedChanges() | ||
{ | ||
return (global::System.Boolean)this._internalUnsavedChanges; | ||
} | ||
public void ResetUnsavedChanges() | ||
{ | ||
this._internalUnsavedChanges = false; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
Moyou.Test/UnsavedChanges/UnsavedChangesAttributeTest/NestedNullable.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using Moyou.Aspects.UnsavedChanges; | ||
|
||
namespace Moyou.CompileTimeTest.UnsavedChanges.UnsavedChangesAttributeTest; | ||
|
||
[UnsavedChanges] | ||
public class NestedNullable | ||
{ | ||
public C C { get; set; } | ||
public C? CNullable { get; set; } | ||
public IEnumerable<C> CNotNullableEnumerable { get; set; } | ||
public IEnumerable<C?> CNotNullableEnumerableNullableT { get; set; } | ||
public IEnumerable<C>? CNullableEnumerable { get; set; } | ||
public IEnumerable<C?>? CNullableEnumerableNullableT { get; set; } | ||
public List<C>? CNullableList { get; set; } | ||
} | ||
|
||
[UnsavedChanges] | ||
public class C | ||
{ | ||
|
||
} |
Oops, something went wrong.