From 180410e076632dcc63579c8b9a4bad38e7e6aa12 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Feb 2024 20:13:52 -0800 Subject: [PATCH] TemplatePart XAML diagnostics (#14180) * Add TemplatePart.IsRequired property and make attribute inherited * Implement TemplatePart XAML diagnostics + tests * Fix new errors in our XAML files * Ignore nested metadata scopes in TemplatePart validator * Update docs comment --- .editorconfig | 6 + .../TransitioningContentControlPage.axaml | 6 +- .../Metadata/TemplatePartAttribute.cs | 9 +- .../Themes/Fluent.xaml | 3 +- .../Themes/Simple.xaml | 3 +- .../Chrome/CaptionButtons.cs | 33 +++-- src/Avalonia.Controls/Chrome/TitleBar.cs | 2 +- src/Avalonia.Controls/ComboBox.cs | 2 +- .../DateTimePickers/DatePickerPresenter.cs | 16 +-- .../DateTimePickers/TimePickerPresenter.cs | 14 +- .../NumericUpDown/NumericUpDown.cs | 2 +- .../Primitives/HeaderedItemsControl.cs | 2 + src/Avalonia.Controls/ProgressBar.cs | 2 +- src/Avalonia.Controls/Slider.cs | 9 +- src/Avalonia.Controls/TabControl.cs | 1 + src/Avalonia.Controls/TextBox.cs | 2 +- src/Avalonia.Controls/ToggleSwitch.cs | 2 +- src/Avalonia.Dialogs/ManagedFileChooser.cs | 4 +- .../Controls/CalendarButton.xaml | 2 +- .../Controls/CalendarDayButton.xaml | 12 +- .../Controls/CalendarItem.xaml | 2 +- .../Controls/CheckBox.xaml | 2 +- .../Controls/DateTimePickerShared.xaml | 2 +- .../Controls/FlyoutPresenter.xaml | 3 +- .../Controls/NotificationCard.xaml | 2 +- .../Controls/ScrollBar.xaml | 3 +- .../Controls/CalendarButton.xaml | 2 +- .../Controls/CalendarDayButton.xaml | 18 +-- .../Controls/DateTimePickerShared.xaml | 2 +- .../Controls/FlyoutPresenter.xaml | 3 +- .../Controls/NotificationCard.xaml | 13 +- .../AvaloniaXamlIlRuntimeCompiler.cs | 19 ++- .../AvaloniaXamlDiagnosticCodes.cs | 3 + .../AvaloniaXamlIlCompiler.cs | 1 + ...aloniaXamlIlControlTemplatePartsChecker.cs | 136 ++++++++++++++++++ ...amlIlControlTemplatePriorityTransformer.cs | 11 +- ...olTemplateTargetTypeMetadataTransformer.cs | 2 +- .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../RuntimeXamlLoaderConfiguration.cs | 13 +- .../Xaml/ControlTemplateTests.cs | 114 ++++++++++++++- 40 files changed, 388 insertions(+), 97 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePartsChecker.cs diff --git a/.editorconfig b/.editorconfig index d5b2badfd5..ee24c310c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -212,6 +212,12 @@ indent_size = 2 avalonia_xaml_diagnostic.AVLN2203.severity = error # StyleInMergedDictionaries avalonia_xaml_diagnostic.AVLN2204.severity = error +# RequiredTemplatePartMissing +avalonia_xaml_diagnostic.AVLN2205.severity = error +# OptionalTemplatePartMissing +avalonia_xaml_diagnostic.AVLN2206.severity = info +# TemplatePartWrongType +avalonia_xaml_diagnostic.AVLN2207.severity = error # Obsolete avalonia_xaml_diagnostic.AVLN5001.severity = error diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml index 541385dd9f..03ef86fb61 100644 --- a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml +++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml @@ -23,10 +23,12 @@ - - diff --git a/src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs b/src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs index 3b8f971713..a2a7e70946 100644 --- a/src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs +++ b/src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs @@ -5,8 +5,6 @@ using System; -#nullable enable - namespace Avalonia.Controls.Metadata { /// @@ -17,7 +15,7 @@ namespace Avalonia.Controls.Metadata /// Style authors should be able to identify the part type used for styling the specific control. /// The part is usually required in the style and should have a specific predefined name. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public sealed class TemplatePartAttribute : Attribute { /// @@ -51,5 +49,10 @@ namespace Avalonia.Controls.Metadata /// in . /// public Type Type { get; set; } + + /// + /// Gets or sets a value indicating whether the template part is mandatory to be present in the template. + /// + public bool IsRequired { get; set; } } } diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 03a692f866..0825feb1a7 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -183,7 +183,8 @@ - - (PART_CloseButton) is { } closeButton) + { + closeButton.Click += (sender, e) => OnClose(); + } - var closeButton = e.NameScope.Get - [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] [TemplatePart("PART_DayDownButton", typeof(RepeatButton))] - [TemplatePart("PART_DayHost", typeof(Panel))] - [TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_DayHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_DayUpButton", typeof(RepeatButton))] [TemplatePart("PART_DismissButton", typeof(Button))] [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] [TemplatePart("PART_MonthDownButton", typeof(RepeatButton))] - [TemplatePart("PART_MonthHost", typeof(Panel))] - [TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MonthHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_MonthUpButton", typeof(RepeatButton))] - [TemplatePart("PART_PickerContainer", typeof(Grid))] + [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] [TemplatePart("PART_YearDownButton", typeof(RepeatButton))] - [TemplatePart("PART_YearHost", typeof(Panel))] - [TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_YearHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_YearUpButton", typeof(RepeatButton))] public class DatePickerPresenter : PickerPresenterBase { diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index 929ad68c24..7f19d0545e 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -11,20 +11,20 @@ namespace Avalonia.Controls /// Defines the presenter used for selecting a time. Intended for use with /// but can be used independently /// - [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] [TemplatePart("PART_DismissButton", typeof(Button))] [TemplatePart("PART_HourDownButton", typeof(RepeatButton))] - [TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_HourUpButton", typeof(RepeatButton))] [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] - [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] - [TemplatePart("PART_PeriodHost", typeof(Panel))] - [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] - [TemplatePart("PART_PickerContainer", typeof(Grid))] - [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)] public class TimePickerPresenter : PickerPresenterBase { /// diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 25d4a90a9d..0edc798494 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. /// [TemplatePart("PART_Spinner", typeof(Spinner))] - [TemplatePart("PART_TextBox", typeof(TextBox))] + [TemplatePart("PART_TextBox", typeof(TextBox), IsRequired = true)] public class NumericUpDown : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index ad4d3324bb..a95280e640 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -10,6 +11,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an with a related header. /// + [TemplatePart("PART_HeaderPresenter", typeof(ContentPresenter))] public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { private IDisposable? _itemsBinding; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 193ed052d4..220931b4f4 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// - [TemplatePart("PART_Indicator", typeof(Border))] + [TemplatePart("PART_Indicator", typeof(Border), IsRequired = true)] [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 6fbfd6f41b..ccb1124e63 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls /// [TemplatePart("PART_DecreaseButton", typeof(Button))] [TemplatePart("PART_IncreaseButton", typeof(Button))] - [TemplatePart("PART_Track", typeof(Track))] + [TemplatePart("PART_Track", typeof(Track), IsRequired = true)] [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { @@ -204,13 +204,10 @@ namespace Avalonia.Controls _pointerMovedDispose?.Dispose(); _decreaseButton = e.NameScope.Find