diff --git a/.github/workflows/CD-Test b/.github/workflows/CD-Test new file mode 100644 index 0000000..892793d --- /dev/null +++ b/.github/workflows/CD-Test @@ -0,0 +1,76 @@ +name: CD_Test + +on: + workflow_dispatch: + # release: + # types: [published] + +env: + TITLE: "Thread-Safe Objects" + DESCRIPTION: "A combination of generic Thread-Safe objects for .Net development." + TAGS: "thunderdesign threading thread%2Dsafe thread%2Dsafety threadsafe maui maui%2Dapp c%2Dsharp dotnet%2Dstandard dotnet%2Dframework dotnet%2Dcore cross%2Dplatform pcl%2Dlibrary xamarin unity csharp net dotnet bindable binding" + FILE_NAME: ex: "ThunderDesign.Net-PCL.Threading" + # FILE_NAME: "${{ github.event.repository.name }}" + REPOSITORY_NAME: ex: "ThunderDesign.Net-PCL.Threading" + # REPOSITORY_NAME: ${{ github.event.repository.name }} + REPOSITORY_OWNER: ex: "ThunderDesign" + # REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_URL: ex: "https://github.com/ThunderDesign" + # GITHUB_URL: ${{ github.server_url }}/${{ github.repository_owner }} + REPOSITORY_URL: ex: "https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading" + # REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository_owner }}/${{ github.event.repository.name }} + + PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}\output + +jobs: + pack: + runs-on: [windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 10.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.1 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v1.0.5 + + - name: Restore NuGet packages.sln + run: nuget restore ./src/${{ env.FILE_NAME}}.sln + + - name: Build Solution + run: msbuild ./src/${{ env.FILE_NAME}}.sln /p:Configuration=Release + + - name: Prepare .nuspec + run: | + $nuspecPath = "src/${{ env.FILE_NAME }}.nuspec.in" + $nuspec = Get-Content $nuspecPath + $nuspec = $nuspec -replace '\$\{\{ env.FILE_NAME \}\}', '${{ env.FILE_NAME }}' + $nuspec = $nuspec -replace '\$\{\{ env.TITLE \}\}', '${{ env.TITLE }}' + $nuspec = $nuspec -replace '\$\{\{ env.DESCRIPTION \}\}', '${{ env.DESCRIPTION }}' + $nuspec = $nuspec -replace '\$\{\{ env.TAGS \}\}', '${{ env.TAGS }}' + $nuspec = $nuspec -replace '\$\{\{ env.GITHUB_URL \}\}', '${{ env.GITHUB_URL }}' + $nuspec = $nuspec -replace '\$\{\{ env.REPOSITORY_URL \}\}', '${{ env.REPOSITORY_URL }}' + Set-Content ThunderDesign.Net-PCL.nuspec $nuspec + shell: pwsh + + - name: Create NuGet Package + run: nuget pack ThunderDesign.Net-PCL.nuspec -Version 3.0.0.0 -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}}.3.0.0.0 + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY}}\${{ env.FILE_NAME}}.3.0.0.0.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 }} diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 117476b..e510aaf 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -30,10 +30,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Setup .NET 8 + - name: Setup .NET 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Setup MSBuild uses: microsoft/setup-msbuild@v1.1 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1d97b34..2b6a5e0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,10 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Setup .NET 8 + - name: Setup .NET 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Setup MSBuild uses: microsoft/setup-msbuild@v1.1 diff --git a/README.md b/README.md index ef55f81..57a03c2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Nuget](https://img.shields.io/nuget/v/ThunderDesign.Net-PCL.Threading)](https://www.nuget.org/packages/ThunderDesign.Net-PCL.Threading) [![License](https://img.shields.io/github/license/ThunderDesign/ThunderDesign.Net-PCL.Threading)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/blob/main/LICENSE) [![NetStandard](https://img.shields.io/badge/.net%20standard-v1.0%20--%20v2.1-blue)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/blob/main/README.md) -[![Net](https://img.shields.io/badge/.net%20-v6.0%20--%20v8.0-blue)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/blob/main/README.md) +[![Net](https://img.shields.io/badge/.net%20-v6.0%20--%20v10.0-blue)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/blob/main/README.md) --- @@ -12,9 +12,9 @@ A combination of generic Thread-Safe objects for .Net development. --- -## 🚀 Now with .NET 8 support and built-in Source Generators! 🚀 +## 🚀 Now with .NET 10 support and built-in Source Generators! 🚀 -> - **.NET 8**: Take advantage of the latest .NET features and performance improvements. +> - **.NET 10**: Take advantage of the latest .NET features and performance improvements. > - **Source Generators**: Eliminate boilerplate and let the library generate thread-safe, bindable properties for you automatically! > > _Get started faster, write less code, and enjoy modern .NET development!_ @@ -275,6 +275,81 @@ public partial class Person : IBindableObject, INotifyPropertyChanged --- +### Advanced: Call Method After Property Changes + +You can call a method when a specific property changes by using the `callMethodAfterSet` parameter in the `[Property]` or `[BindableProperty]` attributes. + +#### Example +```csharp +using ThunderDesign.Net.Threading.Attributes; + +public partial class Person +{ + [BindableProperty(callMethodAfterSet: "OnNameChanged")] + private string _name; + + [Property(callMethodAfterSet:"OnAgeChanged")] + private int _age; + + private void OnNameChanged() + { + // This method is called whenever the value of _name changes. + // You can add any logic here that needs to run when the name changes. + //Console.WriteLine($"Name changed from '{oldValue}' to '{newValue}'"); + } + + private void OnAgeChanged() + { + // This method is called whenever the value of _age changes. + // You can add any logic here that needs to run when the age changes. + //Console.WriteLine($"Age changed from '{oldValue}' to '{newValue}'"); + } +} +``` + +**What gets generated:** + +```csharp +public partial class Person : IBindableObject +{ + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + protected readonly object _Locker = new object(); + + public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + { + this.NotifyPropertyChanged(PropertyChanged, propertyName); + } + + public string Name + { + get { return this.GetProperty(ref _name, _Locker); } + set + { + if (this.SetProperty(ref _name, value, _Locker, true)) + { + this.OnNameChanged(); + } + } + } + + public int Age + { + get { return this.GetProperty(ref _age, _Locker); } + set + { + if (this.SetProperty(ref _age, value, _Locker)) + { + this.OnAgeChanged(); + } + } + } +} +``` + +> This feature is particularly useful if you need to run some code after it updates. This will only get called if value was updated to the property. + +--- + ### Advanced: Static Properties The `[Property]` attribute now supports static fields, allowing you to generate thread-safe static properties with automatic locking mechanisms. diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index 6c64d9e..b2ca412 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -84,16 +84,16 @@ private static (INamedTypeSymbol Class, BindableFieldInfo Bindable, PropertyFiel var bindable = GetBindableField(ctx); if (!bindable.Equals(default(BindableFieldInfo))) return (Class: bindable.ContainingClass, Bindable: bindable, Property: default(PropertyFieldInfo)); - + var prop = PropertyGeneratorHelpers.GetFieldWithAttribute(ctx, "PropertyAttribute"); if (!prop.Equals(default(PropertyFieldInfo))) return (Class: prop.ContainingClass, Bindable: default(BindableFieldInfo), Property: prop); - + return default; } - private static void GenerateSourceCode(SourceProductionContext spc, - (List<(INamedTypeSymbol ClassSymbol, List BindableFields, List PropertyFields)> Left, + private static void GenerateSourceCode(SourceProductionContext spc, + (List<(INamedTypeSymbol ClassSymbol, List BindableFields, List PropertyFields)> Left, Compilation Right) tuple) { var (classGroups, compilation) = tuple; @@ -101,7 +101,22 @@ private static void GenerateSourceCode(SourceProductionContext spc, { if (group.ClassSymbol != null) { - GenerateUnifiedPropertyClass(spc, group.ClassSymbol, group.BindableFields, group.PropertyFields, compilation); + try + { + GenerateUnifiedPropertyClass(spc, group.ClassSymbol, group.BindableFields, group.PropertyFields, compilation); + } + catch (Exception ex) + { + // Report the exception so it shows in the error list + var descriptor = new DiagnosticDescriptor( + id: "TDGEN001", + title: "Source Generation Error", + messageFormat: $"Error generating properties for {group.ClassSymbol.Name}: {ex.Message}", + category: "ThunderDesign.Net-PCL.SourceGenerators", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + spc.ReportDiagnostic(Diagnostic.Create(descriptor, group.ClassSymbol.Locations.FirstOrDefault())); + } } } } @@ -154,17 +169,17 @@ private static void GenerateUnifiedPropertyClass( var source = new StringBuilder(); GenerateClassHeader(source, classSymbol, bindableFields, implementsIBindable); - + // Add infrastructure members if needed GenerateInfrastructureMembers( - source, - bindableFields, - implementsINotify, - implementsIBindable, + source, + bindableFields, + implementsINotify, + implementsIBindable, inheritsThreadObject, - classSymbol, - propertyChangedEventType, - stringTypeSymbol, + classSymbol, + propertyChangedEventType, + stringTypeSymbol, voidTypeSymbol, propertyFields); // Pass propertyFields to check for static properties @@ -179,7 +194,10 @@ private static void GenerateUnifiedPropertyClass( // Generate unique filename var safeClassName = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) .Replace(".", "_") - .Replace("global::", ""); + .Replace("global::", "") + .Replace("<", "_") + .Replace(">", "_") + .Replace(",", ""); var hintName = $"{safeClassName}_AllProperties.g.cs"; context.AddSource(hintName, SourceText.From(source.ToString(), Encoding.UTF8)); @@ -191,96 +209,287 @@ private static bool ValidateFields( List bindableFields, List propertyFields) { - // Check bindable fields - foreach (var info in bindableFields) + try { - // Add this validation for static fields with BindableProperty - if (info.FieldSymbol.IsStatic) + // Check bindable fields + foreach (var info in bindableFields) { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Static fields cannot use [BindableProperty]. Use [Property] instead for field '{info.FieldSymbol.Name}'.") -; return false; - } + try + { + // 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(), - $"Class '{classSymbol.Name}' must be partial to use [BindableProperty]."); - return false; - } - - if (!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name)) - { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Field '{info.FieldSymbol.Name}' must start with '_' or a lowercase letter to use [BindableProperty]."); - return false; + if (!PropertyGeneratorHelpers.IsPartial(classSymbol)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Class '{classSymbol.Name}' must be partial to use [BindableProperty]."); + return false; + } + + if (!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Field '{info.FieldSymbol.Name}' must start with '_' or a lowercase letter to use [BindableProperty]."); + return false; + } + + var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); + if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Property '{propertyName}' already exists in '{classSymbol.Name}'."); + return false; + } + + // Validate CallMethodAfterSet callback if set + var namedArgs = info.AttributeData.NamedArguments; + var callMethodArg = namedArgs.FirstOrDefault(x => x.Key == "CallMethodAfterSet"); + + // Check named arguments first + string callbackName = null; + if (!callMethodArg.Equals(default(KeyValuePair)) && + callMethodArg.Value.Value is string callback && + !string.IsNullOrEmpty(callback)) + { + callbackName = callback; + } + + // If not in named args, check constructor argument at position 3 + if (string.IsNullOrEmpty(callbackName)) + { + var args = info.AttributeData.ConstructorArguments; + if (args.Length > 3 && args[3].Value is string constructorCallback && !string.IsNullOrEmpty(constructorCallback)) + { + callbackName = constructorCallback; + } + } + + if (!string.IsNullOrEmpty(callbackName)) + { + if (!ValidateCallMethodAfterSetCallback(context, classSymbol, info.FieldDeclaration, callbackName, info.FieldSymbol.IsStatic)) + { + return false; + } + } + } + catch (Exception ex) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Error validating field '{info.FieldSymbol.Name}': {ex.Message}"); + return false; + } } - - var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); - if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + + // Check property fields + foreach (var info in propertyFields) { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Property '{propertyName}' already exists in '{classSymbol.Name}'."); - return false; + try + { + if (!PropertyGeneratorHelpers.IsPartial(classSymbol)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Class '{classSymbol.Name}' must be partial to use [Property]."); + return false; + } + + if (!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Field '{info.FieldSymbol.Name}' must start with '_' or a lowercase letter to use [Property]."); + return false; + } + + var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); + if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Property '{propertyName}' already exists in '{classSymbol.Name}'."); + return false; + } + + // Validate CallMethodAfterSet callback if set + var namedArgs = info.AttributeData.NamedArguments; + var callMethodArg = namedArgs.FirstOrDefault(x => x.Key == "CallMethodAfterSet"); + + // Check named arguments first + string callbackName = null; + if (!callMethodArg.Equals(default(KeyValuePair)) && + callMethodArg.Value.Value is string callback && + !string.IsNullOrEmpty(callback)) + { + callbackName = callback; + } + + // If not in named args, check constructor argument at position 1 + if (string.IsNullOrEmpty(callbackName)) + { + var args = info.AttributeData.ConstructorArguments; + if (args.Length > 1 && args[1].Value is string constructorCallback && !string.IsNullOrEmpty(constructorCallback)) + { + callbackName = constructorCallback; + } + } + + if (!string.IsNullOrEmpty(callbackName)) + { + if (!ValidateCallMethodAfterSetCallback(context, classSymbol, info.FieldDeclaration, callbackName, info.FieldSymbol.IsStatic)) + { + return false; + } + } + } + catch (Exception ex) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), + $"Error validating property field '{info.FieldSymbol.Name}': {ex.Message}"); + return false; + } } + + return true; + } + catch (Exception ex) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, classSymbol.Locations.FirstOrDefault(), + $"Error validating fields in class '{classSymbol.Name}': {ex.Message}"); + return false; } + } - // Check property fields - foreach (var info in propertyFields) + private static bool ValidateCallMethodAfterSetCallback( + SourceProductionContext context, + INamedTypeSymbol classSymbol, + FieldDeclarationSyntax fieldDeclaration, + string callbackName, + bool isStatic = false) + { + // Find the method with the given name + var methods = classSymbol.GetMembers(callbackName).OfType(); + + if (!methods.Any()) { - if (!PropertyGeneratorHelpers.IsPartial(classSymbol)) - { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Class '{classSymbol.Name}' must be partial to use [Property]."); - return false; - } - - if (!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name)) - { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Field '{info.FieldSymbol.Name}' must start with '_' or a lowercase letter to use [Property]."); - return false; - } - - var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); - if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) - { - PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), - $"Property '{propertyName}' already exists in '{classSymbol.Name}'."); - return false; - } + PropertyGeneratorHelpers.ReportDiagnostic(context, fieldDeclaration.GetLocation(), + $"CallMethodAfterSet method '{callbackName}' not found in class '{classSymbol.Name}'. " + + $"Method must be defined with signature: private {(isStatic ? "static " : "")}void {callbackName}()"); + return false; + } + + // Check if any method has the correct signature (0 parameters, void return type, static if field is static) + var validMethod = methods.FirstOrDefault(m => + m.Parameters.Length == 0 && + m.ReturnType.SpecialType == SpecialType.System_Void && + m.IsStatic == isStatic); + + if (validMethod == null) + { + PropertyGeneratorHelpers.ReportDiagnostic(context, fieldDeclaration.GetLocation(), + $"CallMethodAfterSet method '{callbackName}' has invalid signature in class '{classSymbol.Name}'. " + + $"Expected: private {(isStatic ? "static " : "")}void {callbackName}(), " + + $"but found method(s) with different signature(s)."); + return false; } - + return true; } private static void GenerateClassHeader( - StringBuilder source, - INamedTypeSymbol classSymbol, - List bindableFields, + StringBuilder source, + INamedTypeSymbol classSymbol, + List bindableFields, bool implementsIBindable) { var ns = classSymbol.ContainingNamespace?.ToDisplayString(); - + 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;"); - + if (bindableFields.Count > 0) source.AppendLine("using ThunderDesign.Net.Threading.Interfaces;"); - source.Append($"partial class {classSymbol.Name}"); - + // Build the class declaration with modifiers and generics + var classModifiers = GetClassModifiers(classSymbol); + var classDeclaration = GetClassDeclarationWithGenerics(classSymbol); + source.Append($"{classModifiers}partial class {classDeclaration}"); + if (bindableFields.Count > 0 && !implementsIBindable) source.Append(" : IBindableObject"); - + source.AppendLine(); source.AppendLine("{"); } + private static string GetClassModifiers(INamedTypeSymbol classSymbol) + { + var modifiers = new StringBuilder(); + + // Add accessibility modifier + switch (classSymbol.DeclaredAccessibility) + { + case Accessibility.Public: + modifiers.Append("public "); + break; + case Accessibility.Private: + modifiers.Append("private "); + break; + case Accessibility.Protected: + modifiers.Append("protected "); + break; + case Accessibility.Internal: + modifiers.Append("internal "); + break; + case Accessibility.ProtectedOrInternal: + modifiers.Append("protected internal "); + break; + case Accessibility.ProtectedAndInternal: + modifiers.Append("private protected "); + break; + } + + // Add static modifier if applicable + if (classSymbol.IsStatic) + modifiers.Append("static "); + + // Add sealed modifier if applicable + if (classSymbol.IsSealed) + modifiers.Append("sealed "); + + // Add abstract modifier if applicable + if (classSymbol.IsAbstract) + modifiers.Append("abstract "); + + return modifiers.ToString(); + } + + private static string GetClassDeclarationWithGenerics(INamedTypeSymbol classSymbol) + { + var sb = new StringBuilder(); + sb.Append(classSymbol.Name); + + // Add type parameters if the class is generic + if (classSymbol.TypeParameters.Length > 0) + { + sb.Append("<"); + for (int i = 0; i < classSymbol.TypeParameters.Length; i++) + { + if (i > 0) + sb.Append(", "); + sb.Append(classSymbol.TypeParameters[i].Name); + } + sb.Append(">"); + } + + return sb.ToString(); + } + private static void GenerateInfrastructureMembers( StringBuilder source, List bindableFields, @@ -295,12 +504,12 @@ 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 && + if (bindableFields.Count > 0 && !implementsINotify && !PropertyGeneratorHelpers.EventExists(classSymbol, "PropertyChanged", propertyChangedEventType)) { source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); @@ -327,7 +536,7 @@ private static void GenerateInfrastructureMembers( { // 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 = """") {{ @@ -343,78 +552,107 @@ private static void GenerateInfrastructureMembers( } private static void GenerateBindableProperties( - StringBuilder source, - List bindableFields, + StringBuilder source, + List bindableFields, INamedTypeSymbol classSymbol, Compilation compilation) { foreach (var info in bindableFields) { - var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); - if (!PropertyGeneratorHelpers.IsPartial(classSymbol) || - !PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) || - PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + try { - continue; - } + var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); + if (!PropertyGeneratorHelpers.IsPartial(classSymbol) || + !PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) || + PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + { + continue; + } - var fieldSymbol = info.FieldSymbol; - var fieldName = fieldSymbol.Name; - - // Use NullableFlowState-aware display string to properly handle nullable types - var typeName = GetNullableAwareTypeName(fieldSymbol.Type, compilation); - - var args = info.AttributeData.ConstructorArguments; - - // 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 > 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"; - string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; - - // 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"; - var notifyArg = notify ? "true" : "false"; - - if (readOnly) - { - GenerateReadOnlyBindableProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg); + var fieldSymbol = info.FieldSymbol; + var fieldName = fieldSymbol.Name; + + // Use NullableFlowState-aware display string to properly handle nullable types + var typeName = GetNullableAwareTypeName(fieldSymbol.Type, compilation); + + var args = info.AttributeData.ConstructorArguments; + var namedArgs = info.AttributeData.NamedArguments; + + // 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); + + // Extract CallMethodAfterSet from BindablePropertyAttribute (position 3) + string callMethodAfterSet = null; + + // Check named arguments first + var callMethodArg = namedArgs.FirstOrDefault(x => x.Key == "CallMethodAfterSet"); + if (!callMethodArg.Equals(default(KeyValuePair)) && + callMethodArg.Value.Value is string callback) + { + callMethodAfterSet = callback; + } + + // If not in named args, check constructor argument at position 3 + if (string.IsNullOrEmpty(callMethodAfterSet) && args.Length > 3 && + args[3].Value is string constructorCallback && + !string.IsNullOrEmpty(constructorCallback)) + { + callMethodAfterSet = constructorCallback; + } + + var getterEnum = args.Length > 4 ? args[4].Value : null; + var setterEnum = args.Length > 5 ? args[5].Value : null; + + // Convert the numeric enum value to its string representation + string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; + string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; + + // 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"; + var notifyArg = notify ? "true" : "false"; + + if (readOnly) + { + GenerateReadOnlyBindableProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg); + } + else + { + GenerateReadWriteBindableProperty( + source, + propertyAccessibilityStr, + typeName, + propertyName, + getterValue, + propertyAccessRaw, + fieldName, + lockerArg, + notifyArg, + alsoNotify, + setterValue, + callMethodAfterSet, + typeName); + } } - else + catch (Exception ex) { - GenerateReadWriteBindableProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg, - notifyArg, - alsoNotify, - setterValue); + System.Diagnostics.Debug.WriteLine($"Error generating bindable property '{info.FieldSymbol.Name}' in class '{classSymbol.Name}': {ex.Message}"); } } } @@ -423,7 +661,7 @@ private static string[] GetAlsoNotifyProperties(ImmutableArray ar { if (args.Length <= 2) return Array.Empty(); - + var arg = args[2]; if (arg.Kind == TypedConstantKind.Array && arg.Values != null) { @@ -432,7 +670,7 @@ private static string[] GetAlsoNotifyProperties(ImmutableArray ar .Where(s => !string.IsNullOrEmpty(s)) .ToArray(); } - + return Array.Empty(); } @@ -468,7 +706,9 @@ private static void GenerateReadWriteBindableProperty( string lockerArg, string notifyArg, string[] alsoNotify, - string setterValue) + string setterValue, + string callMethodAfterSet = null, + string valueTypeName = null) { string getterModifier = getterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) ? "" @@ -478,138 +718,188 @@ private static void GenerateReadWriteBindableProperty( ? "" : ToPropertyAccessibilityString(setterValue); - if (alsoNotify.Length > 0) + // Only wrap in if statement if we have AlsoNotify or CallMethodAfterSet + bool needsIfCheck = alsoNotify.Length > 0 || !string.IsNullOrEmpty(callMethodAfterSet); + + var setterBody = new StringBuilder(); + + if (needsIfCheck) { - var notifyCalls = new StringBuilder(); - foreach (var prop in alsoNotify) + // Only capture old value if we have AlsoNotify; CallMethodAfterSet doesn't need it + if (alsoNotify.Length > 0 || !string.IsNullOrEmpty(callMethodAfterSet)) + { + setterBody.AppendLine($" if (this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}))"); + } + else { - if (!string.IsNullOrEmpty(prop)) - notifyCalls.AppendLine($" this.OnPropertyChanged(\"{prop}\");"); + setterBody.AppendLine($" if (this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}))"); } - source.AppendLine($@" - {propertyAccessibilityStr}{typeName} {propertyName} - {{ - {getterModifier}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }} - {setterModifier}set - {{ - if (this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg})) - {{ -{notifyCalls.ToString().TrimEnd()} - }} - }} - }}"); + setterBody.AppendLine(" {"); + + if (alsoNotify.Length > 0) + { + foreach (var prop in alsoNotify) + { + if (!string.IsNullOrEmpty(prop)) + setterBody.AppendLine($" this.OnPropertyChanged(\"{prop}\");"); + } + } + + if (!string.IsNullOrEmpty(callMethodAfterSet)) + { + setterBody.AppendLine($" this.{callMethodAfterSet}();"); + } + + setterBody.AppendLine(" }"); } else { - source.AppendLine($@" + // Simple case - no if check needed + setterBody.AppendLine($" this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg});"); + } + + source.AppendLine($@" {propertyAccessibilityStr}{typeName} {propertyName} {{ {getterModifier}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }} - {setterModifier}set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }} + {setterModifier}set + {{ +{setterBody.ToString().TrimEnd()} + }} }}"); } - } - private static void GenerateRegularProperties( - StringBuilder source, - List propertyFields, + private static void GenerateRegularProperties( + StringBuilder source, + List propertyFields, INamedTypeSymbol classSymbol, Compilation compilation) { foreach (var info in propertyFields) { - var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); - if (!PropertyGeneratorHelpers.IsPartial(classSymbol) || - !PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) || - PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + try { - continue; - } + var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name); + if (!PropertyGeneratorHelpers.IsPartial(classSymbol) || + !PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) || + PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName)) + { + continue; + } - 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); - - var args = info.AttributeData.ConstructorArguments; - - // 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"; - string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; - - // 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 = isStatic ? (threadSafe ? "_StaticLocker" : "null") : (threadSafe ? "_Locker" : "null"); - - if (isStatic) - { - if (readOnly) + 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); + + var args = info.AttributeData.ConstructorArguments; + var namedArgs = info.AttributeData.NamedArguments; + + // Check if field is readonly (takes precedence over attribute parameter) + var readOnly = fieldSymbol.IsReadOnly; + + var threadSafe = args.Length > 0 && (bool)args[0].Value!; + + // Extract CallMethodAfterSet from PropertyAttribute (position 1) + string callMethodAfterSet = null; + + // Check named arguments first + var callMethodArg = namedArgs.FirstOrDefault(x => x.Key == "CallMethodAfterSet"); + if (!callMethodArg.Equals(default(KeyValuePair)) && + callMethodArg.Value.Value is string callback) { - GenerateReadOnlyStaticProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg); + callMethodAfterSet = callback; } - else + + // If not in named args, check constructor argument at position 1 + if (string.IsNullOrEmpty(callMethodAfterSet) && args.Length > 1 && + args[1].Value is string constructorCallback && + !string.IsNullOrEmpty(constructorCallback)) { - GenerateReadWriteStaticProperty( - source, - propertyAccessibilityStr, - typeName, - propertyName, - getterValue, - propertyAccessRaw, - fieldName, - lockerArg, - setterValue); + callMethodAfterSet = constructorCallback; } - } - else - { - if (readOnly) + + var getterEnum = args.Length > 2 ? args[2].Value : null; + var setterEnum = args.Length > 3 ? args[3].Value : null; + + // Convert the numeric enum value to its string representation + string getterValue = getterEnum != null ? GetAccessibilityName((int)getterEnum) : "Public"; + string setterValue = setterEnum != null ? GetAccessibilityName((int)setterEnum) : "Public"; + + // 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 = isStatic ? (threadSafe ? "_StaticLocker" : "null") : (threadSafe ? "_Locker" : "null"); + + 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, + callMethodAfterSet); + } } 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, + callMethodAfterSet, + typeName); + } } } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error generating property '{info.FieldSymbol.Name}' in class '{classSymbol.Name}': {ex.Message}"); + } } } @@ -643,7 +933,8 @@ private static void GenerateReadWriteStaticProperty( string propertyAccessRaw, string fieldName, string lockerArg, - string setterValue) + string setterValue, + string callMethodAfterSet = null) { string getterModifier = getterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) ? "" @@ -653,12 +944,29 @@ private static void GenerateReadWriteStaticProperty( ? "" : ToPropertyAccessibilityString(setterValue); - source.AppendLine($@" - {propertyAccessibilityStr}static {typeName} {propertyName} - {{ - {getterModifier}get {{ return GetStaticProperty(ref {fieldName}, {lockerArg}); }} - {setterModifier}set {{ SetStaticProperty(ref {fieldName}, value, {lockerArg}); }} - }}"); + if (!string.IsNullOrEmpty(callMethodAfterSet)) + { + source.AppendLine($@" + {propertyAccessibilityStr}static {typeName} {propertyName} + {{ + {getterModifier}get {{ return GetStaticProperty(ref {fieldName}, {lockerArg}); }} + {setterModifier}set + {{ + if (SetStaticProperty(ref {fieldName}, value, {lockerArg})) + {{ + {callMethodAfterSet}(); + }} + }} + }}");} + else + { + source.AppendLine($@" + {propertyAccessibilityStr}static {typeName} {propertyName} + {{ + {getterModifier}get {{ return GetStaticProperty(ref {fieldName}, {lockerArg}); }} + {setterModifier}set {{ SetStaticProperty(ref {fieldName}, value, {lockerArg}); }} + }}");; + } } private static void GenerateReadOnlyProperty( @@ -691,7 +999,9 @@ private static void GenerateReadWriteProperty( string propertyAccessRaw, string fieldName, string lockerArg, - string setterValue) + string setterValue, + string callMethodAfterSet = null, + string valueTypeName = null) { string getterModifier = getterValue.Equals(propertyAccessRaw, StringComparison.OrdinalIgnoreCase) ? "" @@ -701,12 +1011,30 @@ private static void GenerateReadWriteProperty( ? "" : ToPropertyAccessibilityString(setterValue); - source.AppendLine($@" + if (!string.IsNullOrEmpty(callMethodAfterSet)) + { + source.AppendLine($@" + {propertyAccessibilityStr}{typeName} {propertyName} + {{ + {getterModifier}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }} + {setterModifier}set + {{ + if (this.SetProperty(ref {fieldName}, value, {lockerArg})) + {{ + this.{callMethodAfterSet}(); + }} + }} + }}"); + } + else + { + source.AppendLine($@" {propertyAccessibilityStr}{typeName} {propertyName} {{ {getterModifier}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }} {setterModifier}set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }} }}"); + } } private static string GetNullableAwareTypeName(ITypeSymbol typeSymbol, Compilation compilation) diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs index 9eeaf41..0997a87 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs @@ -12,6 +12,7 @@ public sealed class BindablePropertyAttribute : Attribute public bool ThreadSafe { get; } public bool Notify { get; } public string[] AlsoNotify { get; } + public string CallMethodAfterSet { get; } public AccessorAccessibility Getter { get; } public AccessorAccessibility Setter { get; } @@ -19,12 +20,14 @@ public BindablePropertyAttribute( bool threadSafe = true, bool notify = true, string[] alsoNotify = null, + string callMethodAfterSet = null, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { ThreadSafe = threadSafe; Notify = notify; AlsoNotify = alsoNotify ?? Array.Empty(); + CallMethodAfterSet = callMethodAfterSet; Getter = getter; Setter = setter; } diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs index cab07c0..4c06353 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs @@ -8,15 +8,18 @@ namespace ThunderDesign.Net.Threading.Attributes public sealed class PropertyAttribute : Attribute { public bool ThreadSafe { get; } + public string CallMethodAfterSet { get; } public AccessorAccessibility Getter { get; } public AccessorAccessibility Setter { get; } public PropertyAttribute( bool threadSafe = true, + string callMethodAfterSet = null, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { ThreadSafe = threadSafe; + CallMethodAfterSet = callMethodAfterSet; Getter = getter; Setter = setter; } diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj b/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj index c06235c..c31b064 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj @@ -1,8 +1,8 @@  - netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0 - ThunderDesign.Net.Threading.Shared + netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0;net10.0 + ThunderDesign.Net.Threading true true true diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs index b3fde6e..dc59472 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs @@ -292,6 +292,21 @@ public bool IsSynchronized } } +#if NET8_0_OR_GREATER + public new bool TryGetValue(T equalValue, out T actualValue) + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.TryGetValue(equalValue, out actualValue); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } +#endif + public new IEnumerator GetEnumerator() { _ReaderWriterLockSlim.EnterReadLock(); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs index 0127f87..eb51e82 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs @@ -154,7 +154,6 @@ public bool IsSynchronized _ReaderWriterLockSlim.EnterWriteLock(); try { - Queue ts = new Queue(); base.Add(key, value); } finally diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs index b9fb627..f375922 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs @@ -39,6 +39,9 @@ public interface IHashSetThreadSafe : ICollection, IEnumerable, IReadOn new bool SetEquals(IEnumerable other); new void SymmetricExceptWith(IEnumerable other); new void UnionWith(IEnumerable other); +#if NET8_0_OR_GREATER + bool TryGetValue(T equalValue, out T actualValue); +#endif #endregion } #endif diff --git a/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj b/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj index 1c33c8c..d237efc 100644 --- a/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj +++ b/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj @@ -1,7 +1,7 @@  - netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0 + netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0;net10.0 ThunderDesign.Net.Threading README.md @@ -19,7 +19,7 @@ Include="..\ThunderDesign.Net-PCL.SourceGenerators\ThunderDesign.Net-PCL.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" - Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net8.0'" /> + Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0'" />