From 895f368129ea2d120d958ec8d98fc5a1d0065cbf Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Thu, 10 Jul 2025 20:00:29 -0400 Subject: [PATCH 01/12] Removed readonly option from both attributes since we can read from the field itself. Now supports nullable fields and sealed classes. --- .../UnifiedPropertyGenerator.cs | 73 +++++++++++++------ .../Attributes/BindablePropertyAttribute.cs | 3 - .../Attributes/PropertyAttribute.cs | 3 - 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index bbeadd0..3857058 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -168,8 +168,8 @@ private static void GenerateUnifiedPropertyClass( voidTypeSymbol); // Generate properties - GenerateBindableProperties(source, bindableFields, classSymbol); - GenerateRegularProperties(source, propertyFields, classSymbol); + GenerateBindableProperties(source, bindableFields, classSymbol, compilation); + GenerateRegularProperties(source, propertyFields, classSymbol, compilation); source.AppendLine("}"); if (!string.IsNullOrEmpty(classSymbol.ContainingNamespace?.ToDisplayString())) @@ -256,6 +256,7 @@ private static void GenerateClassHeader( if (!string.IsNullOrEmpty(ns)) source.AppendLine($"namespace {ns} {{"); + source.AppendLine("#nullable enable"); source.AppendLine("using ThunderDesign.Net.Threading.Extentions;"); source.AppendLine("using ThunderDesign.Net.Threading.Objects;"); @@ -302,18 +303,22 @@ private static void GenerateInfrastructureMembers( new ITypeSymbol[] { stringTypeSymbol }, voidTypeSymbol)) { - source.AppendLine(@" - public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = """") - { + // Only use 'virtual' if the class is not sealed + string virtualModifier = classSymbol.IsSealed ? "" : "virtual "; + + source.AppendLine($@" + public {virtualModifier}void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = """") + {{ this.NotifyPropertyChanged(PropertyChanged, propertyName); - }"); + }}"); } } private static void GenerateBindableProperties( StringBuilder source, List bindableFields, - INamedTypeSymbol classSymbol) + INamedTypeSymbol classSymbol, + Compilation compilation) { foreach (var info in bindableFields) { @@ -327,17 +332,22 @@ private static void GenerateBindableProperties( var fieldSymbol = info.FieldSymbol; var fieldName = fieldSymbol.Name; - var typeName = fieldSymbol.Type.ToDisplayString(); + + // Use NullableFlowState-aware display string to properly handle nullable types + var typeName = GetNullableAwareTypeName(fieldSymbol.Type, compilation); var args = info.AttributeData.ConstructorArguments; - var readOnly = args.Length > 0 && (bool)args[0].Value!; - var threadSafe = args.Length > 1 && (bool)args[1].Value!; - var notify = args.Length > 2 && (bool)args[2].Value!; + + // Check if field is readonly (takes precedence over attribute parameter) + var readOnly = fieldSymbol.IsReadOnly; + + var threadSafe = args.Length > 0 && (bool)args[0].Value!; + var notify = args.Length > 1 && (bool)args[1].Value!; string[] alsoNotify = GetAlsoNotifyProperties(args); - var getterEnum = args.Length > 4 ? args[4].Value : null; - var setterEnum = args.Length > 5 ? args[5].Value : null; + var getterEnum = args.Length > 3 ? args[3].Value : null; + var setterEnum = args.Length > 4 ? args[4].Value : null; // Convert the numeric enum value to its string representation string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; @@ -381,10 +391,10 @@ private static void GenerateBindableProperties( private static string[] GetAlsoNotifyProperties(ImmutableArray args) { - if (args.Length <= 3) + if (args.Length <= 2) return Array.Empty(); - var arg = args[3]; + var arg = args[2]; if (arg.Kind == TypedConstantKind.Array && arg.Values != null) { return arg.Values @@ -474,7 +484,8 @@ private static void GenerateReadWriteBindableProperty( private static void GenerateRegularProperties( StringBuilder source, List propertyFields, - INamedTypeSymbol classSymbol) + INamedTypeSymbol classSymbol, + Compilation compilation) { foreach (var info in propertyFields) { @@ -488,13 +499,18 @@ private static void GenerateRegularProperties( var fieldSymbol = info.FieldSymbol; var fieldName = fieldSymbol.Name; - var typeName = fieldSymbol.Type.ToDisplayString(); + + // Use NullableFlowState-aware display string to properly handle nullable types + var typeName = GetNullableAwareTypeName(fieldSymbol.Type, compilation); var args = info.AttributeData.ConstructorArguments; - var readOnly = args.Length > 0 && (bool)args[0].Value!; - var threadSafe = args.Length > 1 && (bool)args[1].Value!; - var getterEnum = args.Length > 2 ? args[2].Value : null; - var setterEnum = args.Length > 3 ? args[3].Value : null; + + // Check if field is readonly (takes precedence over attribute parameter) + var readOnly = fieldSymbol.IsReadOnly; + + var threadSafe = args.Length > 0 && (bool)args[0].Value!; + var getterEnum = args.Length > 1 ? args[1].Value : null; + var setterEnum = args.Length > 2 ? args[2].Value : null; // Convert the numeric enum value to its string representation string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; @@ -581,6 +597,21 @@ private static void GenerateReadWriteProperty( }}"); } + private static string GetNullableAwareTypeName(ITypeSymbol typeSymbol, Compilation compilation) + { + // Create a SymbolDisplayFormat that includes nullable annotations + var format = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier + ); + + return typeSymbol.ToDisplayString(format); + } + private static bool ImplementsInterface(INamedTypeSymbol type, string interfaceName) { return type.AllInterfaces.Any(i => i.ToDisplayString() == interfaceName); diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs index 133f952..9eeaf41 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs @@ -9,7 +9,6 @@ namespace ThunderDesign.Net.Threading.Attributes [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public sealed class BindablePropertyAttribute : Attribute { - public bool ReadOnly { get; } public bool ThreadSafe { get; } public bool Notify { get; } public string[] AlsoNotify { get; } @@ -17,14 +16,12 @@ public sealed class BindablePropertyAttribute : Attribute public AccessorAccessibility Setter { get; } public BindablePropertyAttribute( - bool readOnly = false, bool threadSafe = true, bool notify = true, string[] alsoNotify = null, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { - ReadOnly = readOnly; ThreadSafe = threadSafe; Notify = notify; AlsoNotify = alsoNotify ?? Array.Empty(); diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs index e544d01..cab07c0 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs @@ -7,18 +7,15 @@ namespace ThunderDesign.Net.Threading.Attributes [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] public sealed class PropertyAttribute : Attribute { - public bool ReadOnly { get; } public bool ThreadSafe { get; } public AccessorAccessibility Getter { get; } public AccessorAccessibility Setter { get; } public PropertyAttribute( - bool readOnly = false, bool threadSafe = true, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { - ReadOnly = readOnly; ThreadSafe = threadSafe; Getter = getter; Setter = setter; From 403bd50a8b9b313413fdc4b060993338ae9dbecf Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:04:35 -0400 Subject: [PATCH 02/12] Update CD.yml for Testing --- .github/workflows/CD.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 7d19ce4..6634258 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,7 +1,7 @@ name: CD on: - # workflow_dispatch: + workflow_dispatch: release: types: [published] @@ -61,16 +61,16 @@ jobs: shell: pwsh - name: Create NuGet Package - # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.0.17 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.1 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - # name: Package_${{ env.FILE_NAME}}.2.0.17 - # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.0.17.nupkg - name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg + name: Package_${{ env.FILE_NAME}}.2.1.0.1 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.1.nupkg + # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} + # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg - - name: Publish NuGet Package - run: nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey ${{ secrets.NUGET_API_KEY }} + # - name: Publish NuGet Package + # run: nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey ${{ secrets.NUGET_API_KEY }} From 74495d56053b51cd7d2daeee6776a404cc9f0c26 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Thu, 10 Jul 2025 20:25:19 -0400 Subject: [PATCH 03/12] For readonly properties, use getter accessibility as property accessibility. For read-write properties, use the widest accessibility --- .../UnifiedPropertyGenerator.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index 3857058..3928ce4 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -353,7 +353,9 @@ private static void GenerateBindableProperties( string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; - string propertyAccessRaw = GetWidestAccessibility(getterValue, setterValue); + // For readonly properties, use getter accessibility as property accessibility + // For read-write properties, use the widest accessibility + string propertyAccessRaw = readOnly ? getterValue : GetWidestAccessibility(getterValue, setterValue); string propertyAccessibilityStr = ToPropertyAccessibilityString(propertyAccessRaw); var lockerArg = threadSafe ? "_Locker" : "null"; @@ -516,7 +518,9 @@ private static void GenerateRegularProperties( string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; - string propertyAccessRaw = GetWidestAccessibility(getterValue, setterValue); + // For readonly properties, use getter accessibility as property accessibility + // For read-write properties, use the widest accessibility + string propertyAccessRaw = readOnly ? getterValue : GetWidestAccessibility(getterValue, setterValue); string propertyAccessibilityStr = ToPropertyAccessibilityString(propertyAccessRaw); var lockerArg = threadSafe ? "_Locker" : "null"; From ae12e2526be8c31b990ca6e072ac4f767d41de1e Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:26:38 -0400 Subject: [PATCH 04/12] Update CD.yml for Testing --- .github/workflows/CD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 6634258..a2d8ddd 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -61,14 +61,14 @@ jobs: shell: pwsh - name: Create NuGet Package - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.1 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.2 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - name: Package_${{ env.FILE_NAME}}.2.1.0.1 - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.1.nupkg + name: Package_${{ env.FILE_NAME}}.2.1.0.2 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.2.nupkg # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg From 101336e2eb3dca1c94399e7ade8d8380fbc1ea51 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Fri, 11 Jul 2025 00:14:32 -0400 Subject: [PATCH 05/12] Added the ability to use static properties with the Property attribute --- .../UnifiedPropertyGenerator.cs | 218 ++++++++++++++++-- 1 file changed, 195 insertions(+), 23 deletions(-) diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index 3928ce4..8b12f73 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -165,7 +165,8 @@ private static void GenerateUnifiedPropertyClass( classSymbol, propertyChangedEventType, stringTypeSymbol, - voidTypeSymbol); + voidTypeSymbol, + propertyFields); // Pass propertyFields to check for static properties // Generate properties GenerateBindableProperties(source, bindableFields, classSymbol, compilation); @@ -193,6 +194,14 @@ private static bool ValidateFields( // Check bindable fields foreach (var info in bindableFields) { + // Add this validation for static fields with BindableProperty + if (info.FieldSymbol.IsStatic) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Static fields cannot use [BindableProperty]. Use [Property] instead for field '{info.FieldSymbol.Name}'.") +; return false; + } + if (!PropertyGeneratorHelpers.IsPartial(classSymbol)) { PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), @@ -281,8 +290,12 @@ private static void GenerateInfrastructureMembers( INamedTypeSymbol classSymbol, INamedTypeSymbol propertyChangedEventType, ITypeSymbol stringTypeSymbol, - ITypeSymbol voidTypeSymbol) + ITypeSymbol voidTypeSymbol, + List propertyFields) { + // Check if we have any static property fields + bool hasStaticProperties = propertyFields.Any(p => p.FieldSymbol.IsStatic); + // Add event if needed if (bindableFields.Count > 0 && !implementsINotify && !PropertyGeneratorHelpers.EventExists(classSymbol, "PropertyChanged", propertyChangedEventType)) @@ -296,6 +309,12 @@ private static void GenerateInfrastructureMembers( source.AppendLine(" protected readonly object _Locker = new object();"); } + // Add static _StaticLocker if we have static properties + if (hasStaticProperties && !PropertyGeneratorHelpers.FieldExists(classSymbol, "_StaticLocker")) + { + source.AppendLine(" static readonly object _StaticLocker = new object();"); + } + // Add OnPropertyChanged if needed if (bindableFields.Count > 0 && !implementsIBindable && !PropertyGeneratorHelpers.MethodExists( classSymbol, @@ -312,6 +331,12 @@ private static void GenerateInfrastructureMembers( this.NotifyPropertyChanged(PropertyChanged, propertyName); }}"); } + + // Add static property helper methods if we have static properties + if (hasStaticProperties) + { + GenerateStaticPropertyHelpers(source, classSymbol); + } } private static void GenerateBindableProperties( @@ -501,6 +526,7 @@ private static void GenerateRegularProperties( var fieldSymbol = info.FieldSymbol; var fieldName = fieldSymbol.Name; + var isStatic = fieldSymbol.IsStatic; // Use NullableFlowState-aware display string to properly handle nullable types var typeName = GetNullableAwareTypeName(fieldSymbol.Type, compilation); @@ -523,36 +549,115 @@ private static void GenerateRegularProperties( string propertyAccessRaw = readOnly ? getterValue : GetWidestAccessibility(getterValue, setterValue); string propertyAccessibilityStr = ToPropertyAccessibilityString(propertyAccessRaw); - var lockerArg = threadSafe ? "_Locker" : "null"; + var lockerArg = isStatic ? (threadSafe ? "_StaticLocker" : "null") : (threadSafe ? "_Locker" : "null"); - if (readOnly) + if (isStatic) { - GenerateReadOnlyProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg); + if (readOnly) + { + GenerateReadOnlyStaticProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg); + } + else + { + GenerateReadWriteStaticProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg, + setterValue); + } } else { - GenerateReadWriteProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg, - setterValue); + if (readOnly) + { + GenerateReadOnlyProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg); + } + else + { + GenerateReadWriteProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg, + setterValue); + } } } } + private static void GenerateReadOnlyStaticProperty( + StringBuilder source, + string propertyAccessibilityStr, + string typeName, + string propertyName, + string getterValue, + string propertyAccessRaw, + string fieldName, + string lockerArg) + { + string getterModifier = getterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) + ? "" + : ToPropertyAccessibilityString(getterValue); + + source.AppendLine($@" + {propertyAccessibilityStr}static {typeName} {propertyName} + {{ + {getterModifier}get {{ return GetStaticProperty(ref {fieldName}, {lockerArg}); }} + }}"); + } + + private static void GenerateReadWriteStaticProperty( + StringBuilder source, + string propertyAccessibilityStr, + string typeName, + string propertyName, + string getterValue, + string propertyAccessRaw, + string fieldName, + string lockerArg, + string setterValue) + { + string getterModifier = getterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) + ? "" + : ToPropertyAccessibilityString(getterValue); + + string setterModifier = setterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) + ? "" + : ToPropertyAccessibilityString(setterValue); + + source.AppendLine($@" + {propertyAccessibilityStr}static {typeName} {propertyName} + {{ + {getterModifier}get {{ return GetStaticProperty(ref {fieldName}, {lockerArg}); }} + {setterModifier}set {{ SetStaticProperty(ref {fieldName}, value, {lockerArg}); }} + }}"); + } + private static void GenerateReadOnlyProperty( StringBuilder source, string propertyAccessibilityStr, @@ -638,6 +743,73 @@ private static string GetWidestAccessibility(string getter, string setter) return getterRank >= setterRank ? getter : setter; } + private static void GenerateStaticPropertyHelpers(StringBuilder source, INamedTypeSymbol classSymbol) + { + // Check if GetStaticProperty method already exists + var genericMethodExists = classSymbol.GetMembers() + .OfType() + .Any(m => m.Name == "GetStaticProperty" && m.IsStatic && m.IsGenericMethod); + + if (!genericMethodExists) + { + source.AppendLine(@" + public static T GetStaticProperty( + ref T backingStore, + object? lockObj = null) + { + bool lockWasTaken = false; + try + { + if (lockObj != null) + System.Threading.Monitor.Enter(lockObj, ref lockWasTaken); + return backingStore; + } + finally + { + if (lockWasTaken) + System.Threading.Monitor.Exit(lockObj!); + } + }"); + } + + // Check if SetStaticProperty method already exists + var setMethodExists = classSymbol.GetMembers() + .OfType() + .Any(m => m.Name == "SetStaticProperty" && m.IsStatic && m.IsGenericMethod); + + if (!setMethodExists) + { + source.AppendLine(@" + public static bool SetStaticProperty( + ref T backingStore, + T value, + object? lockObj = null, + [System.Runtime.CompilerServices.CallerMemberName] string propertyName = """") + { + bool lockWasTaken = false; + try + { + if (lockObj != null) + System.Threading.Monitor.Enter(lockObj, ref lockWasTaken); + if (System.Collections.Generic.EqualityComparer.Default.Equals(backingStore, value)) + { + return false; + } + else + { + backingStore = value; + return true; + } + } + finally + { + if (lockWasTaken) + System.Threading.Monitor.Exit(lockObj!); + } + }"); + } + } + private struct BindableFieldInfo { public IFieldSymbol FieldSymbol { get; set; } From 6632c2595043a205f4f13dba1f80183aaa9d163d Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:15:58 -0400 Subject: [PATCH 06/12] Update CD.yml for Testing --- .github/workflows/CD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index a2d8ddd..3199f3c 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -61,14 +61,14 @@ jobs: shell: pwsh - name: Create NuGet Package - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.2 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.3 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - name: Package_${{ env.FILE_NAME}}.2.1.0.2 - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.2.nupkg + name: Package_${{ env.FILE_NAME}}.2.1.0.3 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.3.nupkg # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg From 0febec9719851db7f4e78c00e8830f6cedc3d459 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Fri, 11 Jul 2025 00:30:40 -0400 Subject: [PATCH 07/12] Now only added _Locker for non static fields --- .../UnifiedPropertyGenerator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index 8b12f73..ba9d0a2 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -295,6 +295,9 @@ private static void GenerateInfrastructureMembers( { // Check if we have any static property fields bool hasStaticProperties = propertyFields.Any(p => p.FieldSymbol.IsStatic); + + // Check if we have any non-static fields that need the instance locker + bool hasNonStaticFields = bindableFields.Count > 0 || propertyFields.Any(p => !p.FieldSymbol.IsStatic); // Add event if needed if (bindableFields.Count > 0 && !implementsINotify && @@ -303,8 +306,8 @@ private static void GenerateInfrastructureMembers( source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); } - // Add _Locker if needed - if ((!inheritsThreadObject) && !PropertyGeneratorHelpers.FieldExists(classSymbol, "_Locker")) + // Add _Locker if needed (only for non-static fields) + if (hasNonStaticFields && (!inheritsThreadObject) && !PropertyGeneratorHelpers.FieldExists(classSymbol, "_Locker")) { source.AppendLine(" protected readonly object _Locker = new object();"); } From cbddf86f36ce2573332138e805207ffa128087fa Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:31:59 -0400 Subject: [PATCH 08/12] Update CD.yml for Testing --- .github/workflows/CD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 3199f3c..ed3ae47 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -61,14 +61,14 @@ jobs: shell: pwsh - name: Create NuGet Package - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.3 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.4 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - name: Package_${{ env.FILE_NAME}}.2.1.0.3 - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.3.nupkg + name: Package_${{ env.FILE_NAME}}.2.1.0.4 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.4.nupkg # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg From 3fc7d1bd38ddcfe14ccbf09e1550ae9134bcb5fb Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Fri, 11 Jul 2025 00:52:19 -0400 Subject: [PATCH 09/12] Only generate SetStaticProperty if we have writable static fields --- .../UnifiedPropertyGenerator.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index ba9d0a2..6c64d9e 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -338,7 +338,7 @@ private static void GenerateInfrastructureMembers( // Add static property helper methods if we have static properties if (hasStaticProperties) { - GenerateStaticPropertyHelpers(source, classSymbol); + GenerateStaticPropertyHelpers(source, classSymbol, propertyFields); } } @@ -746,8 +746,11 @@ private static string GetWidestAccessibility(string getter, string setter) return getterRank >= setterRank ? getter : setter; } - private static void GenerateStaticPropertyHelpers(StringBuilder source, INamedTypeSymbol classSymbol) + private static void GenerateStaticPropertyHelpers(StringBuilder source, INamedTypeSymbol classSymbol, List propertyFields) { + // Check if we have any static fields that are NOT readonly (need SetStaticProperty) + bool hasWritableStaticFields = propertyFields.Any(p => p.FieldSymbol.IsStatic && !p.FieldSymbol.IsReadOnly); + // Check if GetStaticProperty method already exists var genericMethodExists = classSymbol.GetMembers() .OfType() @@ -775,14 +778,17 @@ public static T GetStaticProperty( }"); } - // Check if SetStaticProperty method already exists - var setMethodExists = classSymbol.GetMembers() - .OfType() - .Any(m => m.Name == "SetStaticProperty" && m.IsStatic && m.IsGenericMethod); - - if (!setMethodExists) + // Only generate SetStaticProperty if we have writable static fields + if (hasWritableStaticFields) { - source.AppendLine(@" + // Check if SetStaticProperty method already exists + var setMethodExists = classSymbol.GetMembers() + .OfType() + .Any(m => m.Name == "SetStaticProperty" && m.IsStatic && m.IsGenericMethod); + + if (!setMethodExists) + { + source.AppendLine(@" public static bool SetStaticProperty( ref T backingStore, T value, @@ -810,6 +816,7 @@ public static bool SetStaticProperty( System.Threading.Monitor.Exit(lockObj!); } }"); + } } } From 1b6cfecda2d1c03631c6b8fe97762f4ccc530bb7 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:53:36 -0400 Subject: [PATCH 10/12] Update CD.yml for Testing --- .github/workflows/CD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index ed3ae47..26f414a 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -61,14 +61,14 @@ jobs: shell: pwsh - name: Create NuGet Package - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.4 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.5 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - name: Package_${{ env.FILE_NAME}}.2.1.0.4 - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.4.nupkg + name: Package_${{ env.FILE_NAME}}.2.1.0.5 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.5.nupkg # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg From 93488c0c282dc9a95d26c70b95a475b259d081fd Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Fri, 11 Jul 2025 01:18:20 -0400 Subject: [PATCH 11/12] Updated ReadMe file to include static properties --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 224a196..ef55f81 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,52 @@ public partial class Person : IBindableObject, INotifyPropertyChanged --- +### Advanced: Static Properties + +The `[Property]` attribute now supports static fields, allowing you to generate thread-safe static properties with automatic locking mechanisms. + +#### Example +```csharp +using ThunderDesign.Net.Threading.Attributes; + +public partial class AppSettings +{ + [Property] + private static string _applicationName = "MyApp"; + + [Property(getter: AccessorAccessibility.Internal)] + private static readonly string _version = "1.0.0"; +} +``` + +**What gets generated:** + +```csharp +public partial class AppSettings +{ + static readonly object _StaticLocker = new object(); + + public static string ApplicationName + { + get { return GetStaticProperty(ref _applicationName, _StaticLocker); } + set { SetStaticProperty(ref _applicationName, value, _StaticLocker); } + } + + internal static string Version + { + get { return GetStaticProperty(ref _version, _StaticLocker); } + } + + // Helper methods for static property access + public static T GetStaticProperty(ref T backingStore, object? lockObj = null) { /* ... */ } + public static bool SetStaticProperty(ref T backingStore, T value, object? lockObj = null) { /* ... */ } +} +``` + +> **Note:** Static properties are only supported with the `[Property]` attribute. Use the `readonly` field modifier to create read-only static properties. + +--- + ## Installation Grab the latest [ThunderDesign.Net-PCL.Threading NuGet](https://www.nuget.org/packages/ThunderDesign.Net-PCL.Threading) package and install in your solution. From 24b03c1de108abf0d9d9ec5f8e2973eba706e376 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain <98920689+ShawnLaMountain@users.noreply.github.com> Date: Fri, 11 Jul 2025 01:20:07 -0400 Subject: [PATCH 12/12] Update CD.yml for production --- .github/workflows/CD.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 26f414a..117476b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,7 +1,7 @@ name: CD on: - workflow_dispatch: + # workflow_dispatch: release: types: [published] @@ -61,16 +61,16 @@ jobs: shell: pwsh - name: Create NuGet Package - run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.5 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + # run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 2.1.0.5 -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version ${{ github.event.release.tag_name }} -OutputDirectory ${{ env.PACKAGE_OUTPUT_DIRECTORY }} - name: Archive NuGet Package uses: actions/upload-artifact@v4 with: - name: Package_${{ env.FILE_NAME}}.2.1.0.5 - path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.5.nupkg - # name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} - # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg + # name: Package_${{ env.FILE_NAME}}.2.1.0.5 + # path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.2.1.0.5.nupkg + name: Package_${{ env.FILE_NAME}}.${{ github.event.release.tag_name }} + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg - # - name: Publish NuGet Package - # run: nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey ${{ secrets.NUGET_API_KEY }} + - name: Publish NuGet Package + run: nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.${{ github.event.release.tag_name }}.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey ${{ secrets.NUGET_API_KEY }}