Skip to content

C# source generator for Avalonia properties#21797

Open
maxkatz6 wants to merge 20 commits into
mainfrom
props-source-generator
Open

C# source generator for Avalonia properties#21797
maxkatz6 wants to merge 20 commits into
mainfrom
props-source-generator

Conversation

@maxkatz6

@maxkatz6 maxkatz6 commented Jul 18, 2026

Copy link
Copy Markdown
Member

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:

public partial class MyControl : AvaloniaObject
{
    [StyledProperty]
    public partial string? Header { get; set; }

    [DirectProperty] // read only allowed
    public partial int SelectedIndex { get; private set; }

    [AttachedProperty]
    public static partial int GetOrder(Visual element);

    // Const default values, and callback methods
    [StyledProperty(DefaultValue = 100, ChangedMethodName = nameof(OnWidthChanged))]
    public partial double Width { get; set; }

    private partial void OnWidthChanged(double oldValue, double newValue) { /* … */ }
}

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.Register call, 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:

  • It's still verbose, very
  • Nowhere to write XML docs for the property (can be worked around by copying static definition docs to the property, but these typically are different)
  • "Go to definition" on the property goes to the generated code.

Callbacks definition

In the current design, OnChanged, Validate and Coerce callbacks 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.Mvvm goes 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:

  • Get method with an attribute. Consistent with styled/direct properties, but looks more unusual.
  • Class attribute on the whole owner type.

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:
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class StyledPropertyAttribute : Attribute
{
    public Type? AddOwnerFrom { get; set; }
    public object? DefaultValue { get; set; }
    public BindingMode DefaultBindingMode { get; set; }
    public bool Inherits { get; set; }
    public bool EnableDataValidation { get; set; }

    public string? ChangedMethodName { get; set; }
    public string? ValidateMethodName { get; set; }
    public string? CoerceMethodName { get; set; }
}

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class DirectPropertyAttribute : Attribute
{
    public Type? AddOwnerFrom { get; set; }
    public object? UnsetValue { get; set; }
    public BindingMode DefaultBindingMode { get; set; }
    public bool EnableDataValidation { get; set; }

    public string? ChangedMethodName { get; set; }
}

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class AttachedPropertyAttribute : Attribute
{
    public Type? AddOwnerFrom { get; set; }
    public object? DefaultValue { get; set; }
    public BindingMode DefaultBindingMode { get; set; }
    public bool Inherits { get; set; }

    public string? ChangedMethodName { get; set; }
    public string? ValidateMethodName { get; set; }
    public string? CoerceMethodName { get; set; }
}
Diagnostics:
ID Severity Rule Code fix
AVP2001 Error Containing type must derive from AvaloniaObject -
AVP2002 Error Conflicting attribute args (Inherits/ValidateMethodName with AddOwnerFrom, or multiple generator attributes) -
AVP2003 Error DefaultValue/UnsetValue constant not convertible to the property type -
AVP2004 Error AddOwnerFrom type has no compatible {Name}Property -
AVP2005 Error Invalid attached accessor shape (static partial T Get{Name}(THost)) -
AVP2006 Error Callback name doesn't resolve to an implemented partial method Add callback stub
AVP2007 Error Member/containing type not partial, or language version < C# 13 Make partial
AVP2008 Error Invalid property shape (static/indexer/init-only/missing accessor/…) -
AVP2100 Warning Name doubles the suffix (CountProperty -> CountPropertyProperty) -
AVP2101 Warning Non-public setter on a styled property isn't read-only - use [DirectProperty] -
AVP2102 Hidden Manual property can be converted to a generated one (migration hint) - (would be useful, but technically complicated with callbacks and metadata overrides)

@maxkatz6 maxkatz6 added feature area-analyzers needs-api-review The PR adds new public APIs that should be reviewed. labels Jul 18, 2026

namespace TestNs
{
partial class MyControl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; } = "";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +14 to +17
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()!));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it use nameof to refer to a separate static readonly field containing the default?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that, yes. But:

  • I am not sure if it's more readable and maintainable than OverrideDefaultValue option, from the user perspective. It might be better for the performance though (I am not sure how big is overhead for OverrideDefaultValue)
  • Do you mean that we should have two Default properties on the attribute? I.e. DefaultValue for const and DefaultValueField (DefaultValueFieldName ?) for readonly fields? Or the only one? Either way - it adds complexity to the attribute.

@maxkatz6

Copy link
Copy Markdown
Member Author

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.

@maxkatz6 maxkatz6 added this to the 12.2 milestone Jul 18, 2026
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0067516-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@mklts

mklts commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Nice idea! Good job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-analyzers feature needs-api-review The PR adds new public APIs that should be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants