C# source generator for Avalonia properties#21797
Conversation
…ertyAttribute attribtues
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| namespace TestNs | ||
| { | ||
| partial class MyControl |
There was a problem hiding this comment.
This and other test files are a good example of how generated code looks like.
| public partial class MyControl : AvaloniaObject | ||
| { | ||
| [DirectProperty] | ||
| public partial string Text { get; set; } = ""; |
There was a problem hiding this comment.
With C# 14 it's possible to set value of the direct property inline.
C# compiler only allows this syntax on auto-properties and properties with field keyword as a backing field. It's an important nuance, why this syntax won't work on C# 13.
| private static readonly global::System.IDisposable _dockChangedReg = | ||
| global::Avalonia.AvaloniaObjectExtensions.AddClassHandler<global::Avalonia.Visual, int>( | ||
| DockProperty.Changed, | ||
| static (sender, e) => OnDockChanged(sender, e.OldValue.GetValueOrDefault()!, e.NewValue.GetValueOrDefault()!)); |
There was a problem hiding this comment.
Changed callbacks are implemented via AddClassHandler.
This way it does not conflict with OnPropertyChanged user overrides and/or instance/static constructors.
But it does add a slight overhead.
There was a problem hiding this comment.
This might be the time to add changed handlers to the property definition call like WPF has.
| { | ||
| static MyControl() | ||
| { | ||
| PaddingProperty.OverrideDefaultValue<MyControl>(new Thickness(4)); |
There was a problem hiding this comment.
Only const values can be used in C# attributes syntax. Unfortunately, it means that any non-const default value have to be defined via OverrideDefaultValue (or by manually writing property definition).
There was a problem hiding this comment.
Could it use nameof to refer to a separate static readonly field containing the default?
There was a problem hiding this comment.
We can do that, yes. But:
- I am not sure if it's more readable and maintainable than
OverrideDefaultValueoption, from the user perspective. It might be better for the performance though (I am not sure how big is overhead forOverrideDefaultValue) - Do you mean that we should have two
Defaultproperties on the attribute? I.e.DefaultValuefor const andDefaultValueField(DefaultValueFieldName?) for readonly fields? Or the only one? Either way - it adds complexity to the attribute.
|
So far, I tested this source generator on some internal libraries. And, eventually, I want to migrate Avalonia itself to these attributes - this PR only migrates some control catalog pages. |
|
You can test this PR using the following package version. |
|
Nice idea! Good job! |
What does the pull request do?
Defining Avalonia properties requires a lot of boilerplate - static field, Register call, instance property with GetValue/SetValue calls.
This PR allows to write a partial property with an attribute instead, with registration and getter/setter being source generated.
Final output is more or less identical (with some nuances around default value and callbacks).
Enabled by default. Opt-out with
<AvaloniaPropertyGeneratorIsEnabled>false</AvaloniaPropertyGeneratorIsEnabled>.Note: this default might be changed before the release.
Example:
Technical details
There are several existing dep-property source generators for XAML frameworks, including Avalonia. As far as I can tell, none is official, so we can't just copy by the rule of precedent.
Main choices in the design include - property-driver vs registration-driven, callbacks definition, attached properties shape.
Registration-driven vs property-driver
Before C# 13 first option was more or less the only option, with the lack of partial properties. User would define static field with
AvaloniaProperty.Registercall, and instance property with getter/setter is generated, not visible in the user code directly.And second option, available with C# 13, is partial property with an attribute, with property registration source generated.
First option has multiple issues, part of the reason why we never had this feature implemented way before:
Callbacks definition
In the current design,
OnChanged,ValidateandCoercecallbacks are linked by a string name property on the attribute.It works fine, allowing user to set any custom name, source generator will create a partial definition, and compile-time enforce implementation to exist.
It's not the only option though. For example,
CommunityToolkit.Mvvmgoes a different way, and detects partial methods defined in the user code.So, there it's possible to define
partial void OnMyPropertyChanged(int value)- where source generator will detect this method and automatically wire it with the setter. I was considering this approach, but not everybody likes this "magic-like" approach, where you need to know ahead how this callback should be defined, with little help from the compiler to enforce correctness. On the opposite side, this way we could support more than one overload option.Attached properties shape
Probably, an elephant in the room for some.
I see two main options here:
On a coin flip I went with the first option, as it also allows to avoid hardcoding property name as a string, and allows to define property attributes on the same member level of the type (property and methods).
Also, I was later reminded that XamlX compiler looks into
Get**methods when resolving attached properties, not the definition itself.Analyzers Code-fixers
This PR includes list of useful analyzers (see the table below), highlighting manual properties that can converted, and possible issues with generated ones.
And it's also possible to write extensive code fixers that can automatically convert manual into generated definitions. Unfortunately, I gave up on this idea for now - neither me nor AI can do it nicely, without too much of spaghetti and corner cases. And only supporting basic property code-fixers is just confusing.
Details
API surface:
Diagnostics:
AvaloniaObjectInherits/ValidateMethodNamewithAddOwnerFrom, or multiple generator attributes)DefaultValue/UnsetValueconstant not convertible to the property typeAddOwnerFromtype has no compatible{Name}Propertystatic partial T Get{Name}(THost))partial, or language version < C# 13CountProperty->CountPropertyProperty)[DirectProperty]