Avalonia.NameGenerator
Avalonia.NameGenerator copied to clipboard
Generate properties for templates using TemplatePartAttribute
There is a new attribute ported from WPF. It will be useful to have optional code generation for "parts" of templates. As an example, user code:
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
public partial class TextBox
{
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (IsFocused)
{
TextPresenter?.ShowCaret(); // TextPresenter is generated
}
}
}
Additional generated code, adds new property and sets value to it:
#nullable enable
public partial class TextBox
{
private TextPresenter? TextPresenter { get; set; }
static TextBox()
{
TemplateAppliedEvent.AddClassHandler<TextBox>((control, args) =>
{
control.TextPresenter = args.NameScope.Get<TextPresenter>("PART_TextPresenter");
});
}
}
Problems:
- Only one static ctor can be created for class, if we want to handle TemplateAppliedEvent to fill these properties.
- Only one override of OnTemplateApplied method is possible, if we want to use it to fill these properties.
Which means, we most likely will need to modify Avalonia to add yet another way to read namescope after template was applied.
@kekekeks @worldbeater if you have any thoughts.
I think code generation is a good option here and like the ideas. I'll have to read more into what WinUI is doing when I first noticed this (https://github.com/AvaloniaUI/Avalonia/issues/7432#issuecomment-1066183609). They are also generating an enum so there are no magic strings anywhere.
See: https://github.com/microsoft/microsoft-ui-xaml/blob/2a6d2d69c809c141ca7916c9730fd5305487501e/dev/TeachingTip/TeachingTipTemplatePartHelpers.h
There is also a pattern that is used for UWP that removes strings -- it uses nameof() of the fields instead. This wouldn't work for these ideas because someone still has to define the name whether it's a field or a string so this is just FYI:
[TemplatePart(Name = nameof(ColorPicker.AlphaChannelSlider), Type = typeof(ColorPickerSlider))]
[TemplatePart(Name = nameof(ColorPicker.AlphaChannelTextBox), Type = typeof(TextBox))]
public partial class ColorPicker : Microsoft.UI.Xaml.Controls.ColorPicker
{
private TextBox AlphaChannelTextBox;
private ColorPickerSlider AlphaChannelSlider;
protected override void OnApplyTemplate()
{
this.AlphaChannelTextBox = this.GetTemplateChild<TextBox>(nameof(AlphaChannelTextBox));
this.AlphaChannelSlider = this.GetTemplateChild<ColorPickerSlider>(nameof(AlphaChannelSlider));
}
}
https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI.Controls.Input/ColorPicker/ColorPicker.cs
How exactly would custom OnApplyTemplate
logic work with such generated code? Should we generate a partial method to be called from OnApplyTemplate
?
How exactly would custom OnApplyTemplate logic work with such generated code? Should we generate a partial method to be called from OnApplyTemplate?
This is the best solution I can think of as well. Just do it like InitializeComponent() in a separate partial class file. It might be better actually because it is opt-in for each control. They have to explicitly call the new method from OnApplyTemplate().
add yet another way to read namescope after template was applied.
Just pass the arguments.
protected void InitializeTemplateParts(TemplateAppliedEventArgs e)
{
}
This method can be added to the avalonia base control class, and called just before OnApplyTemplate. As an option. Probably with some "hide from intellisense" attributes. A bit hacky, but transparent for user.
protected virtual void InitializeTemplateParts(TemplateAppliedEventArgs e);
This method can be added to the avalonia base control class, and called just before OnApplyTemplate.
Good idea! That would be totally transparent as you said. I think we have a working solution all ideas together.
Regarding the implementation details of the new generator, I guess it is worth introducing a new class similar to AvaloniaNameGenerator
named e.g. TemplatePartGenerator
, then adding a new option to GeneratorOptions
that would control if the new generator is enabled, and then invoking the generator in the composition root.
The code that scans the assembly and finds all classes annotated with the TemplatePart
attribute can be found here https://github.com/reactivemarbles/ObservableEvents/blob/main/src/ReactiveMarbles.ObservableEvents.SourceGenerator/SyntaxReceiver.cs#L17 Most likely we'll need to implement ISyntaxReceiver
that receives all relevant classes with TemplatePart
attributes.