Skip to content

Commit e42d14a

Browse files
Added AccessorAccessibility to Attributes
1 parent e683868 commit e42d14a

9 files changed

Lines changed: 208 additions & 73 deletions

File tree

README.md

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,7 @@ public partial class Person
117117
}
118118
```
119119

120-
**What gets generated:**
121-
- A public `Name` property with thread-safe getter/setter and `INotifyPropertyChanged` support.
122-
- A public `Age` property with thread-safe getter/setter.
120+
**What gets generated:**
123121

124122
```csharp
125123
using System.ComponentModel;
@@ -142,7 +140,7 @@ public partial class Person : IBindableObject, INotifyPropertyChanged
142140
public int Age
143141
{
144142
get { return this.GetProperty(ref _age, _Locker); }
145-
set { this.SetProperty(ref _age, value, _Locker); }
143+
set { this.SetProperty(ref _age, value, _Locker, true); }
146144
}
147145
}
148146
```
@@ -155,11 +153,116 @@ person.Name = "Alice";
155153
person.Age = 30; // PropertyChanged event will be raised for Name changes if you subscribe to it.
156154
```
157155

158-
159156
**No need to manually implement** property notification, thread safety, or boilerplate code—the generator does it for you!
160157

161-
> For more advanced scenarios, you can use attribute parameters to control property behavior (e.g., read-only, also notify other properties, etc.).
158+
> For more advanced scenarios, you can use attribute parameters to control property behavior (e.g., read-only, also notify other properties, or control accessor visibility).
159+
160+
### Advanced: Customizing Property Accessors
161+
162+
You can now control the visibility of generated property getters and setters using the `AccessorAccessibility` enum.
163+
This allows you to generate properties with `private`, `protected`, `internal`, or other C# accessibilities for the `get` and `set` accessors.
164+
165+
#### Example
166+
167+
```csharp
168+
using ThunderDesign.Net.Threading.Attributes;
169+
using ThunderDesign.Net_PCL.Threading.Shared.Enums;
170+
171+
public partial class Person
172+
{
173+
// Public getter, private setter [BindableProperty(getter: AccessorAccessibility.Public, setter: AccessorAccessibility.Private)] private string _name;
174+
// Internal getter, protected setter
175+
[Property(getter: AccessorAccessibility.Internal, setter: AccessorAccessibility.Protected)]
176+
private int _age;
177+
}
178+
```
179+
180+
**What gets generated:**
181+
182+
```csharp
183+
public partial class Person
184+
{
185+
public string Name { get { return this.GetProperty(ref _name, _Locker); } private set { this.SetProperty(ref _name, value, _Locker, true); } }
186+
internal int Age
187+
{
188+
get { return this.GetProperty(ref _age, _Locker); }
189+
protected set { this.SetProperty(ref _age, value, _Locker); }
190+
}
191+
}
192+
```
193+
194+
> The default for both getter and setter is `public` if not specified.
195+
196+
**Available options for `AccessorAccessibility`:**
197+
- `Public`
198+
- `Private`
199+
- `Protected`
200+
- `Internal`
201+
- `ProtectedInternal`
202+
- `PrivateProtected`
203+
204+
### Advanced: Notify Other Properties
205+
206+
You can now notify other properties when a specific property changes by using the `alsoNotify` parameter in the `[BindableProperty]` attribute.
207+
208+
#### Example
209+
210+
```csharp
211+
using ThunderDesign.Net.Threading.Attributes;
212+
213+
public partial class Person
214+
{
215+
[BindableProperty(alsoNotify: new[] { nameof(DisplayName) })]
216+
private string _firstName;
217+
218+
[BindableProperty(alsoNotify: new[] { nameof(DisplayName) })]
219+
private string _lastName;
220+
221+
public string DisplayName => $"{FirstName} {LastName}";
222+
}
223+
```
224+
225+
**What gets generated:**
226+
227+
```csharp
228+
public partial class Person : IBindableObject, INotifyPropertyChanged
229+
{
230+
public event PropertyChangedEventHandler PropertyChanged;
231+
232+
protected readonly object _Locker = new object();
233+
234+
public string FirstName
235+
{
236+
get { return this.GetProperty(ref _firstName, _Locker); }
237+
set
238+
{
239+
if (this.SetProperty(ref _firstName, value, _Locker, true))
240+
{
241+
this.OnPropertyChanged(nameof(DisplayName));
242+
}
243+
}
244+
}
245+
246+
public string LastName
247+
{
248+
get { return this.GetProperty(ref _lastName, _Locker); }
249+
set
250+
{
251+
this.SetProperty(ref _lastName, value, _Locker, true);
252+
OnPropertyChanged(nameof(DisplayName));
253+
}
254+
}
255+
256+
public string DisplayName => $"{FirstName} {LastName}";
257+
258+
protected void OnPropertyChanged(string propertyName)
259+
{
260+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
261+
}
262+
}
263+
```
162264

265+
> This feature is particularly useful for computed properties like `DisplayName` that depend on other properties.
163266
164267
----
165268

src/ThunderDesign.Net-PCL.SourceGenerators/PropertyGeneratorHelpers.cs renamed to src/ThunderDesign.Net-PCL.SourceGenerators/Helpers/PropertyGeneratorHelpers.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Microsoft.CodeAnalysis.CSharp.Syntax;
44
using System.Linq;
55

6-
namespace ThunderDesign.Net_PCL.SourceGenerators
6+
namespace ThunderDesign.Net_PCL.SourceGenerators.Helpers
77
{
88
internal static class PropertyGeneratorHelpers
99
{
@@ -74,7 +74,7 @@ public static PropertyFieldInfo GetFieldWithAttribute(GeneratorSyntaxContext con
7474
}
7575
}
7676
}
77-
return default(PropertyFieldInfo);
77+
return default;
7878
}
7979

8080
// Rule 2: Field must start with "_" followed by a letter, or a lowercase letter
@@ -123,17 +123,17 @@ public static bool EventExists(INamedTypeSymbol classSymbol, string eventName, I
123123
public static bool MethodExists(
124124
INamedTypeSymbol classSymbol,
125125
string methodName,
126-
ITypeSymbol[]? parameterTypes = null,
127-
ITypeSymbol? returnType = null)
126+
ITypeSymbol[] parameterTypes = null,
127+
ITypeSymbol returnType = null)
128128
{
129129
return classSymbol.GetMembers()
130130
.OfType<IMethodSymbol>()
131131
.Any(m =>
132132
m.Name == methodName &&
133133
(parameterTypes == null ||
134-
(m.Parameters.Length == parameterTypes.Length &&
134+
m.Parameters.Length == parameterTypes.Length &&
135135
m.Parameters.Select(p => p.Type.ToDisplayString())
136-
.SequenceEqual(parameterTypes.Select(t => t.ToDisplayString())))) &&
136+
.SequenceEqual(parameterTypes.Select(t => t.ToDisplayString()))) &&
137137
(returnType == null || SymbolEqualityComparer.Default.Equals(m.ReturnType, returnType))
138138
);
139139
}

src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Linq;
88
using System.Text;
9+
using ThunderDesign.Net_PCL.SourceGenerators.Helpers;
910

1011
namespace ThunderDesign.Net_PCL.SourceGenerators
1112
{
@@ -200,10 +201,25 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
200201
}");
201202
}
202203

204+
// Helper to convert AccessorAccessibility to C# keyword
205+
static string ToAccessorString(object accessor)
206+
{
207+
var value = accessor?.ToString() ?? "Public";
208+
return value switch
209+
{
210+
"Public" => "",
211+
"Private" => "private ",
212+
"Protected" => "protected ",
213+
"Internal" => "internal ",
214+
"ProtectedInternal" => "protected internal ",
215+
"PrivateProtected" => "private protected ",
216+
_ => ""
217+
};
218+
}
219+
203220
// Generate all bindable properties
204221
foreach (var info in bindableFields)
205222
{
206-
// Skip if any rule failed (diagnostic already reported)
207223
var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name);
208224
if (!PropertyGeneratorHelpers.IsPartial(classSymbol) ||
209225
!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) ||
@@ -216,13 +232,14 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
216232
var fieldName = fieldSymbol.Name;
217233
var typeName = fieldSymbol.Type.ToDisplayString();
218234

219-
var readOnly = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
220-
var threadSafe = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
221-
var notify = info.AttributeData.ConstructorArguments.Length > 2 && (bool)info.AttributeData.ConstructorArguments[2].Value!;
235+
var args = info.AttributeData.ConstructorArguments;
236+
var readOnly = args.Length > 0 && (bool)args[0].Value!;
237+
var threadSafe = args.Length > 1 && (bool)args[1].Value!;
238+
var notify = args.Length > 2 && (bool)args[2].Value!;
222239
string[] alsoNotify = null;
223-
if (info.AttributeData.ConstructorArguments.Length > 3)
240+
if (args.Length > 3)
224241
{
225-
var arg = info.AttributeData.ConstructorArguments[3];
242+
var arg = args[3];
226243
if (arg.Kind == TypedConstantKind.Array && arg.Values != null)
227244
{
228245
alsoNotify = arg.Values
@@ -234,14 +251,21 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
234251
if (alsoNotify == null)
235252
alsoNotify = new string[0];
236253

254+
// Default to Public if not present
255+
var getter = args.Length > 4 ? args[4].Value : null;
256+
var setter = args.Length > 5 ? args[5].Value : null;
257+
258+
var getterStr = ToAccessorString(getter);
259+
var setterStr = ToAccessorString(setter);
260+
237261
var lockerArg = threadSafe ? "_Locker" : "null";
238262
var notifyArg = notify ? "true" : "false";
239263
if (readOnly)
240264
{
241265
source.AppendLine($@"
242266
public {typeName} {propertyName}
243267
{{
244-
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
268+
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
245269
}}");
246270
}
247271
else
@@ -256,7 +280,7 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
256280
notifyCalls.AppendLine($" this.OnPropertyChanged(\"{prop}\");");
257281
}
258282
setAccessor = $@"
259-
set
283+
{setterStr}set
260284
{{
261285
if (this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}))
262286
{{
@@ -266,13 +290,13 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
266290
}
267291
else
268292
{
269-
setAccessor = $"set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }}";
293+
setAccessor = $"{setterStr}set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }}";
270294
}
271295

272296
source.AppendLine($@"
273297
public {typeName} {propertyName}
274298
{{
275-
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
299+
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
276300
{setAccessor}
277301
}}");
278302
}
@@ -294,25 +318,31 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
294318
var fieldName = fieldSymbol.Name;
295319
var typeName = fieldSymbol.Type.ToDisplayString();
296320

297-
var readOnly = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
298-
var threadSafe = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
321+
var args = info.AttributeData.ConstructorArguments;
322+
var readOnly = args.Length > 0 && (bool)args[0].Value!;
323+
var threadSafe = args.Length > 1 && (bool)args[1].Value!;
324+
var getter = args.Length > 2 ? args[2].Value : null;
325+
var setter = args.Length > 3 ? args[3].Value : null;
326+
327+
var getterStr = ToAccessorString(getter);
328+
var setterStr = ToAccessorString(setter);
299329

300330
var lockerArg = threadSafe ? "_Locker" : "null";
301331
if (readOnly)
302332
{
303333
source.AppendLine($@"
304334
public {typeName} {propertyName}
305335
{{
306-
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
336+
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
307337
}}");
308338
}
309339
else
310340
{
311341
source.AppendLine($@"
312342
public {typeName} {propertyName}
313343
{{
314-
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
315-
set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }}
344+
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
345+
{setterStr}set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }}
316346
}}");
317347
}
318348
}

src/ThunderDesign.Net-PCL.Threading/Attributes/BindablePropertyAttribute.cs renamed to src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using ThunderDesign.Net.Threading.Enums;
23

34
namespace ThunderDesign.Net_PCL.Threading.Attributes
45
{
@@ -9,17 +10,23 @@ public sealed class BindablePropertyAttribute : Attribute
910
public bool ThreadSafe { get; }
1011
public bool Notify { get; }
1112
public string[] AlsoNotify { get; }
13+
public AccessorAccessibility Getter { get; }
14+
public AccessorAccessibility Setter { get; }
1215

1316
public BindablePropertyAttribute(
1417
bool readOnly = false,
1518
bool threadSafe = true,
1619
bool notify = true,
17-
string[] alsoNotify = null)
20+
string[] alsoNotify = null,
21+
AccessorAccessibility getter = AccessorAccessibility.Public,
22+
AccessorAccessibility setter = AccessorAccessibility.Public)
1823
{
1924
ReadOnly = readOnly;
2025
ThreadSafe = threadSafe;
2126
Notify = notify;
2227
AlsoNotify = alsoNotify ?? new string[0];
28+
Getter = getter;
29+
Setter = setter;
2330
}
2431
}
2532
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using ThunderDesign.Net.Threading.Enums;
5+
6+
namespace ThunderDesign.Net_PCL.Threading.Attributes
7+
{
8+
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
9+
public sealed class PropertyAttribute : Attribute
10+
{
11+
public bool ReadOnly { get; }
12+
public bool ThreadSafe { get; }
13+
public AccessorAccessibility Getter { get; }
14+
public AccessorAccessibility Setter { get; }
15+
16+
public PropertyAttribute(
17+
bool readOnly = false,
18+
bool threadSafe = true,
19+
AccessorAccessibility getter = AccessorAccessibility.Public,
20+
AccessorAccessibility setter = AccessorAccessibility.Public)
21+
{
22+
ReadOnly = readOnly;
23+
ThreadSafe = threadSafe;
24+
Getter = getter;
25+
Setter = setter;
26+
}
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ThunderDesign.Net.Threading.Enums
2+
{
3+
public enum AccessorAccessibility
4+
{
5+
Public,
6+
Private,
7+
Protected,
8+
Internal,
9+
ProtectedInternal,
10+
PrivateProtected
11+
}
12+
}

0 commit comments

Comments
 (0)