Browse Source

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
pull/14736/head
Max Katz 2 years ago
committed by GitHub
parent
commit
180410e076
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .editorconfig
  2. 6
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
  3. 9
      src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs
  4. 3
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  5. 3
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  6. 33
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  7. 2
      src/Avalonia.Controls/Chrome/TitleBar.cs
  8. 2
      src/Avalonia.Controls/ComboBox.cs
  9. 16
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  10. 14
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  11. 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  12. 2
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  13. 2
      src/Avalonia.Controls/ProgressBar.cs
  14. 9
      src/Avalonia.Controls/Slider.cs
  15. 1
      src/Avalonia.Controls/TabControl.cs
  16. 2
      src/Avalonia.Controls/TextBox.cs
  17. 2
      src/Avalonia.Controls/ToggleSwitch.cs
  18. 4
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  19. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
  20. 12
      src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
  21. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  22. 2
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  23. 2
      src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml
  24. 3
      src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
  25. 2
      src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml
  26. 3
      src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml
  27. 2
      src/Avalonia.Themes.Simple/Controls/CalendarButton.xaml
  28. 18
      src/Avalonia.Themes.Simple/Controls/CalendarDayButton.xaml
  29. 2
      src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml
  30. 3
      src/Avalonia.Themes.Simple/Controls/FlyoutPresenter.xaml
  31. 13
      src/Avalonia.Themes.Simple/Controls/NotificationCard.xaml
  32. 19
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  33. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlDiagnosticCodes.cs
  34. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  35. 136
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePartsChecker.cs
  36. 11
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs
  37. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  38. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  39. 13
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs
  40. 114
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

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

6
samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml

@ -23,10 +23,12 @@
<ColumnDefinition Width="Auto" SharedSizeGroup="HeaderCol" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Header}"
<ContentPresenter x:Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Grid.Column="0"
VerticalAlignment="Center" />
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Grid.Column="1"
VerticalAlignment="Center" />
</Grid>

9
src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs

@ -5,8 +5,6 @@
using System;
#nullable enable
namespace Avalonia.Controls.Metadata
{
/// <summary>
@ -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.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class TemplatePartAttribute : Attribute
{
/// <summary>
@ -51,5 +49,10 @@ namespace Avalonia.Controls.Metadata
/// in <see cref="Name"/>.
/// </summary>
public Type Type { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the template part is mandatory to be present in the template.
/// </summary>
public bool IsRequired { get; set; }
}
}

3
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -183,7 +183,8 @@
<ColumnDefinition Width="Auto" MinWidth="{DynamicResource DataGridSortIconMinWidth}" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"

3
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -66,7 +66,8 @@
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ColumnDefinitions="*,Auto">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"

33
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -99,19 +99,28 @@ namespace Avalonia.Controls.Chrome
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<Button>(PART_CloseButton) is { } closeButton)
{
closeButton.Click += (sender, e) => OnClose();
}
var closeButton = e.NameScope.Get<Button>(PART_CloseButton);
var restoreButton = e.NameScope.Get<Button>(PART_RestoreButton);
var minimizeButton = e.NameScope.Get<Button>(PART_MinimizeButton);
var fullScreenButton = e.NameScope.Get<Button>(PART_FullScreenButton);
closeButton.Click += (sender, e) => OnClose();
restoreButton.Click += (sender, e) => OnRestore();
minimizeButton.Click += (sender, e) => OnMinimize();
fullScreenButton.Click += (sender, e) => OnToggleFullScreen();
restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
_restoreButton = restoreButton;
if (e.NameScope.Find<Button>(PART_RestoreButton) is { } restoreButton)
{
restoreButton.Click += (sender, e) => OnRestore();
restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
_restoreButton = restoreButton;
}
if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton)
{
minimizeButton.Click += (sender, e) => OnMinimize();
}
if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton)
{
fullScreenButton.Click += (sender, e) => OnToggleFullScreen();
}
}
}
}

2
src/Avalonia.Controls/Chrome/TitleBar.cs

@ -8,7 +8,7 @@ namespace Avalonia.Controls.Chrome
/// <summary>
/// Draws a titlebar when managed client decorations are enabled.
/// </summary>
[TemplatePart("PART_CaptionButtons", typeof(CaptionButtons))]
[TemplatePart("PART_CaptionButtons", typeof(CaptionButtons), IsRequired = true)]
[PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
public class TitleBar : TemplatedControl
{

2
src/Avalonia.Controls/ComboBox.cs

@ -17,7 +17,7 @@ namespace Avalonia.Controls
/// <summary>
/// A drop-down list control.
/// </summary>
[TemplatePart("PART_Popup", typeof(Popup))]
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{

16
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -13,22 +13,22 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/>
/// </summary>
[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
{

14
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
/// <see cref="TimePicker"/> but can be used independently
/// </summary>
[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
{
/// <summary>

2
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.
/// </summary>
[TemplatePart("PART_Spinner", typeof(Spinner))]
[TemplatePart("PART_TextBox", typeof(TextBox))]
[TemplatePart("PART_TextBox", typeof(TextBox), IsRequired = true)]
public class NumericUpDown : TemplatedControl
{
/// <summary>

2
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
/// <summary>
/// Represents an <see cref="ItemsControl"/> with a related header.
/// </summary>
[TemplatePart("PART_HeaderPresenter", typeof(ContentPresenter))]
public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
{
private IDisposable? _itemsBinding;

2
src/Avalonia.Controls/ProgressBar.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control used to indicate the progress of an operation.
/// </summary>
[TemplatePart("PART_Indicator", typeof(Border))]
[TemplatePart("PART_Indicator", typeof(Border), IsRequired = true)]
[PseudoClasses(":vertical", ":horizontal", ":indeterminate")]
public class ProgressBar : RangeBase
{

9
src/Avalonia.Controls/Slider.cs

@ -45,7 +45,7 @@ namespace Avalonia.Controls
/// </summary>
[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<Button>("PART_DecreaseButton");
_track = e.NameScope.Find<Track>("PART_Track");
_track = e.NameScope.Get<Track>("PART_Track");
_increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
if (_track != null)
{
_track.IgnoreThumbDrag = true;
}
_track.IgnoreThumbDrag = true;
if (_decreaseButton != null)
{

1
src/Avalonia.Controls/TabControl.cs

@ -18,6 +18,7 @@ namespace Avalonia.Controls
/// A tab control that displays a tab strip along with the content of the selected tab.
/// </summary>
[TemplatePart("PART_ItemsPresenter", typeof(ItemsPresenter))]
[TemplatePart("PART_SelectedContentHost", typeof(ContentPresenter))]
public class TabControl : SelectingItemsControl, IContentPresenterHost
{
private object? _selectedContent;

2
src/Avalonia.Controls/TextBox.cs

@ -24,7 +24,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a control that can be used to display or edit unformatted text.
/// </summary>
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
[TemplatePart("PART_TextPresenter", typeof(TextPresenter), IsRequired = true)]
[TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost

2
src/Avalonia.Controls/ToggleSwitch.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A Toggle Switch control.
/// </summary>
[TemplatePart("PART_MovingKnobs", typeof(Panel))]
[TemplatePart("PART_MovingKnobs", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_OffContentPresenter", typeof(ContentPresenter))]
[TemplatePart("PART_OnContentPresenter", typeof(ContentPresenter))]
[TemplatePart("PART_SwitchKnob", typeof(Panel))]

4
src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -11,8 +11,8 @@ using Avalonia.LogicalTree;
namespace Avalonia.Dialogs
{
[TemplatePart("PART_QuickLinks", typeof(Control))]
[TemplatePart("PART_Files", typeof(ListBox))]
[TemplatePart("PART_QuickLinks", typeof(Control), IsRequired = true)]
[TemplatePart("PART_Files", typeof(ListBox), IsRequired = true)]
public class ManagedFileChooser : TemplatedControl
{
private Control? _quickLinksRoot;

2
src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml

@ -35,7 +35,7 @@
<!-- To mimic WinUI SystemFocusVisual, Focus visual is drawn outside the bounds of the item -->
<Border Name="Root" Background="{TemplateBinding Background}"
BorderThickness="0" ClipToBounds="True">
<ContentPresenter Name="Content"
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

12
src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml

@ -36,12 +36,12 @@
<Border Name="Root" Background="{TemplateBinding Background}"
BorderThickness="0" ClipToBounds="True">
<ContentControl Name="Content"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}" />
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}" />
</Border>

2
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

@ -31,7 +31,7 @@
<Setter Property="Template">
<ControlTemplate>
<!-- HCA was changed here to ensure nav arrows display correctly -->
<ContentPresenter Name="Text" Background="{TemplateBinding Background}"
<ContentPresenter Name="PART_ContentPresenter" Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource CalendarViewNavigationButtonBorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"

2
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -57,7 +57,7 @@
</Panel>
</Viewbox>
</Grid>
<ContentPresenter x:Name="ContentPresenter"
<ContentPresenter x:Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"

2
src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml

@ -55,7 +55,7 @@
that appear opaque. Not sure how MS does it though I suspect this is it
but source isn't MIT yet, so this is my solution -->
<Border Background="{TemplateBinding Background}">
<ContentPresenter x:Name="ContentPresenter"
<ContentPresenter x:Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource DateTimePickerFlyoutButtonBorderBrush}"
BorderThickness="{DynamicResource DateTimeFlyoutButtonBorderThickness}"

3
src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml

@ -26,7 +26,8 @@
CornerRadius="{TemplateBinding CornerRadius}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

2
src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml

@ -25,7 +25,7 @@
ClipToBounds="True">
<DockPanel>
<Panel x:Name="PART_HeaderBar" Height="4" Background="{DynamicResource NotificationCardProgressBackgroundBrush}" DockPanel.Dock="Top" />
<ContentControl Name="PART_Content" MinHeight="64" Content="{TemplateBinding Content}" />
<ContentPresenter Name="PART_ContentPresenter" MinHeight="64" Content="{TemplateBinding Content}" />
</DockPanel>
</Border>
</Border>

3
src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml

@ -63,7 +63,8 @@
<Setter Property="TextElement.Foreground" Value="{DynamicResource ScrollBarButtonArrowForeground}"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource ScrollBarButtonBackground}"
<ContentPresenter x:Name="PART_ContentPresenter"
Background="{DynamicResource ScrollBarButtonBackground}"
BorderBrush="{DynamicResource ScrollBarButtonBorderBrush}"
Content="{TemplateBinding Content}"/>
</ControlTemplate>

2
src/Avalonia.Themes.Simple/Controls/CalendarButton.xaml

@ -33,7 +33,7 @@
Opacity="0.5" />
<!-- Focusable="False" -->
<ContentPresenter Name="Content"
<ContentPresenter x:Name="PART_ContentPresenter"
Margin="1,0,1,1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

18
src/Avalonia.Themes.Simple/Controls/CalendarDayButton.xaml

@ -36,15 +36,15 @@
IsVisible="False"
Opacity="0.5" />
<ContentControl Name="Content"
Margin="5,1,5,1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontSize="{TemplateBinding FontSize}"
Foreground="{TemplateBinding Foreground}"
Opacity="1" />
<ContentPresenter Name="PART_ContentPresenter"
Margin="5,1,5,1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontSize="{TemplateBinding FontSize}"
Foreground="{TemplateBinding Foreground}"
Opacity="1" />
<Path Name="BlackoutVisual"
Margin="3"

2
src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml

@ -64,7 +64,7 @@
but source isn't MIT yet, so this is my solution
-->
<Border Background="{TemplateBinding Background}">
<ContentPresenter x:Name="ContentPresenter"
<ContentPresenter x:Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

3
src/Avalonia.Themes.Simple/Controls/FlyoutPresenter.xaml

@ -20,7 +20,8 @@
CornerRadius="{TemplateBinding CornerRadius}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ContentPresenter Margin="{TemplateBinding Padding}"
<ContentPresenter x:Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="Stretch"

13
src/Avalonia.Themes.Simple/Controls/NotificationCard.xaml

@ -13,15 +13,14 @@
<ControlTemplate>
<LayoutTransformControl Name="PART_LayoutTransformControl"
UseRenderTransform="True">
<Border Margin="8,8,0,0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentControl Name="PART_Content"
<ContentPresenter Name="PART_ContentPresenter"
MinHeight="150"
Padding="8,8,0,0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}" />
</Border>
</LayoutTransformControl>
</ControlTemplate>
</Setter>

19
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs

@ -215,14 +215,27 @@ namespace Avalonia.Markup.Xaml.XamlIl
HandleDiagnostic = (diagnostic) =>
{
var runtimeDiagnostic = new RuntimeXamlDiagnostic(diagnostic.Code.ToString(),
(RuntimeXamlDiagnosticSeverity)diagnostic.Severity,
diagnostic.Severity switch
{
XamlDiagnosticSeverity.None => RuntimeXamlDiagnosticSeverity.Info,
XamlDiagnosticSeverity.Warning => RuntimeXamlDiagnosticSeverity.Warning,
XamlDiagnosticSeverity.Error => RuntimeXamlDiagnosticSeverity.Error,
XamlDiagnosticSeverity.Fatal => RuntimeXamlDiagnosticSeverity.Fatal,
_ => throw new ArgumentOutOfRangeException()
},
diagnostic.Title, diagnostic.LineNumber, diagnostic.LinePosition)
{
Document = diagnostic.Document
};
var newSeverity =
(XamlDiagnosticSeverity?)configuration.DiagnosticHandler?.Invoke(runtimeDiagnostic) ??
diagnostic.Severity;
configuration.DiagnosticHandler?.Invoke(runtimeDiagnostic) switch
{
RuntimeXamlDiagnosticSeverity.Info => XamlDiagnosticSeverity.None,
RuntimeXamlDiagnosticSeverity.Warning => XamlDiagnosticSeverity.Warning,
RuntimeXamlDiagnosticSeverity.Error => XamlDiagnosticSeverity.Error,
RuntimeXamlDiagnosticSeverity.Fatal => XamlDiagnosticSeverity.Fatal,
_ => (XamlDiagnosticSeverity?)null
} ?? diagnostic.Severity;
diagnostic = diagnostic with { Severity = newSeverity };
diagnostics.Add(diagnostic);
return newSeverity;

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlDiagnosticCodes.cs

@ -25,6 +25,9 @@ internal static class AvaloniaXamlDiagnosticCodes
public const string PropertyPathError = "AVLN2202";
public const string DuplicateSetterError = "AVLN2203";
public const string StyleInMergedDictionaries = "AVLN2204";
public const string RequiredTemplatePartMissing = "AVLN2205";
public const string OptionalTemplatePartMissing = "AVLN2206";
public const string TemplatePartWrongType = "AVLN2207";
// XAML emit errors 3000-3999.
public const string EmitError = "AVLN3000";

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -76,6 +76,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// After everything else
InsertBefore<NewObjectTransformer>(
new AddNameScopeRegistration(),
new AvaloniaXamlIlControlTemplatePartsChecker(),
new AvaloniaXamlIlDataContextTypeTransformer(),
new AvaloniaXamlIlBindingPathTransformer(),
new AvaloniaXamlIlCompiledBindingsMetadataRemover()

136
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePartsChecker.cs

@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
internal class AvaloniaXamlIlControlTemplatePartsChecker : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is AvaloniaXamlIlTargetTypeMetadataNode on
&& on.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate
// Styles with template selector will also return ScopeTypes.ControlTemplate, so we need to double check.
&& on.Value.Type.GetClrType() == context.GetAvaloniaTypes().ControlTemplate))
return node;
var targetType = on.TargetType.GetClrType();
var templateParts = ResolveTemplateParts(targetType);
if (templateParts.Count == 0)
return node;
var visitor = new TemplatePartVisitor();
node.VisitChildren(visitor);
foreach (var pair in templateParts)
{
var name = pair.Key;
var (expectedType, isRequired) = pair.Value;
if (!visitor.TryGetValue(name, out var res))
{
if (isRequired)
{
context.ReportDiagnostic(new XamlDiagnostic(
AvaloniaXamlDiagnosticCodes.RequiredTemplatePartMissing,
XamlDiagnosticSeverity.Error,
$"Required template part with x:Name '{name}' must be defined on '{targetType.Name}' ControlTemplate.",
node));
}
else
{
context.ReportDiagnostic(new XamlDiagnostic(
AvaloniaXamlDiagnosticCodes.OptionalTemplatePartMissing,
XamlDiagnosticSeverity.None,
$"Optional template part with x:Name '{name}' can be defined on '{targetType.Name}' ControlTemplate.",
node));
}
continue;
}
if (expectedType is not null
&& !expectedType.IsAssignableFrom(res.type))
{
context.ReportDiagnostic(new XamlDiagnostic(
AvaloniaXamlDiagnosticCodes.TemplatePartWrongType,
XamlDiagnosticSeverity.Error,
$"Template part '{name}' is expected to be assignable to '{expectedType.Name}', but actual type is {res.type.Name}.",
res.line));
}
}
return node;
}
private static Dictionary<string, (IXamlType? type, bool isRequired)> ResolveTemplateParts(IXamlType targetType)
{
var dictionary = new Dictionary<string, (IXamlType? type, bool isRequired)>();
// Custom Attributes go in order from current type to base type. It should be possible to override parent template parts.
foreach (var attr in targetType.GetAllCustomAttributes())
{
if (attr.Type.Name == "TemplatePartAttribute")
{
if (!attr.Properties.TryGetValue("Name", out var nameObj))
{
nameObj = attr.Parameters.FirstOrDefault();
}
if (!attr.Properties.TryGetValue("Type", out var typeObj))
{
typeObj = attr.Parameters.Skip(1).FirstOrDefault();
}
attr.Properties.TryGetValue("IsRequired", out var isRequiredObj);
if (nameObj is string { Length :> 0 } name
&& !dictionary.ContainsKey(name))
{
dictionary.Add(name, (typeObj as IXamlType, isRequiredObj as bool? == true));
}
}
}
return dictionary;
}
private class TemplatePartVisitor : Dictionary<string, (IXamlType type, IXamlLineInfo line)>, IXamlAstVisitor
{
private int _metadataScopeLevel = 0;
private Stack<IXamlAstNode> _parents = new();
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (_metadataScopeLevel == 1
&& node is AvaloniaNameScopeRegistrationXamlIlNode nameScopeRegistration
&& nameScopeRegistration.Name is XamlAstTextNode textNode)
{
this[textNode.Text] = (nameScopeRegistration.TargetType, textNode);
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node)
{
_parents.Push(node);
if (node is NestedScopeMetadataNode)
{
_metadataScopeLevel++;
}
}
void IXamlAstVisitor.Pop()
{
var oldParent = _parents.Pop();
if (oldParent is NestedScopeMetadataNode)
{
_metadataScopeLevel--;
}
}
}
}

11
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs

@ -21,7 +21,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
// - The property has a single value
if (node is XamlPropertyAssignmentNode prop &&
prop.Property is XamlIlAvaloniaProperty avaloniaProperty &&
context.ParentNodes().Any(IsControlTemplate) &&
context.ParentNodes().Any(c => c is AvaloniaXamlIlTargetTypeMetadataNode
{
ScopeType: AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate
}) &&
prop.Values.Count == 1)
{
var priorityValueSetters = new List<IXamlPropertySetter>();
@ -48,11 +51,5 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
return node;
}
private static bool IsControlTemplate(IXamlAstNode node)
{
return node is AvaloniaXamlIlTargetTypeMetadataNode tt &&
tt.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate;
}
}
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

@ -11,7 +11,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlAstObjectNode on
&& on.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.Templates.ControlTemplate"))
&& on.Type.GetClrType() == context.GetAvaloniaTypes().ControlTemplate))
return node;
var tt = on.Children.OfType<XamlAstXamlPropertyValueNode>().FirstOrDefault(ch =>
ch.Property.GetClrProperty().Name == "TargetType");

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -120,6 +120,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ControlTheme { get; }
public IXamlType WindowTransparencyLevel { get; }
public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@ -264,6 +265,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
UriConstructor = Uri.GetConstructor(new List<IXamlType>() { cfg.WellKnownTypes.String, UriKind });
Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style");
ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate");
IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1");
}
}

13
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs

@ -39,13 +39,18 @@ public class RuntimeXamlLoaderConfiguration
public enum RuntimeXamlDiagnosticSeverity
{
None = 0,
/// <summary>
/// Something that is an issue, as determined by some authority,
/// but is not surfaced through normal means.
/// There may be different mechanisms that act on these issues.
/// </summary>
Info = 1,
/// <summary>
/// Diagnostic is reported as a warning.
/// </summary>
Warning = 1,
Warning,
/// <summary>
/// Diagnostic is reported as an error.
/// Compilation process is continued until the end of the parsing and transforming stage, throwing an aggregated exception of all errors.

114
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@ -276,7 +279,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
TargetType='{x:Type ContentControl}'>
<ContentPresenter Content='{TemplateBinding Content}' />
<ContentPresenter x:Name='PART_ContentPresenter' Content='{TemplateBinding Content}' />
</ControlTemplate>
";
var template = AvaloniaRuntimeXamlLoader.Parse<ControlTemplate>(xaml);
@ -293,7 +296,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
TargetType='ContentControl'>
<ContentPresenter Content='{TemplateBinding Content}' />
<ContentPresenter x:Name='PART_ContentPresenter' Content='{TemplateBinding Content}' />
</ControlTemplate>
";
var template = AvaloniaRuntimeXamlLoader.Parse<ControlTemplate>(xaml);
@ -337,6 +340,103 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var templateResult = template.Build(new TemplatedControl());
Assert.Null(templateResult);
}
[Fact]
public void ControlTemplate_Outputs_Error_When_Missing_TemplatePart()
{
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:controls='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
TargetType='controls:CustomButtonWithParts'>
<Border Name='PART_Typo_MainContentBorder'>
<ContentPresenter Name='PART_ContentPresenter'
Content='{TemplateBinding Content}'/>
</Border>
</ControlTemplate>";
var diagnostics = new List<RuntimeXamlDiagnostic>();
AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), new RuntimeXamlLoaderConfiguration
{
LocalAssembly = typeof(XamlIlTests).Assembly,
DiagnosticHandler = diagnostic =>
{
diagnostics.Add(diagnostic);
return diagnostic.Severity;
}
});
var warning = Assert.Single(diagnostics);
Assert.Equal(RuntimeXamlDiagnosticSeverity.Info, warning.Severity);
Assert.Contains("'PART_MainContentBorder'", warning.Title);
}
[Fact]
public void ControlTemplate_Outputs_Error_When_Using_Wrong_Type_With_TemplatePart()
{
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:controls='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
TargetType='controls:CustomControlWithParts'>
<Border Name='PART_MainContentBorder'>
<ContentControl Name='PART_ContentPresenter'
Content='{TemplateBinding Content}'/>
</Border>
</ControlTemplate>";
var diagnostics = new List<RuntimeXamlDiagnostic>();
Assert.ThrowsAny<XmlException>(() => AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), new RuntimeXamlLoaderConfiguration
{
LocalAssembly = typeof(XamlIlTests).Assembly,
DiagnosticHandler = diagnostic =>
{
diagnostics.Add(diagnostic);
return diagnostic.Severity;
}
}));
var warning = Assert.Single(diagnostics);
Assert.Equal(RuntimeXamlDiagnosticSeverity.Error, warning.Severity);
Assert.Contains("'ContentPresenter'", warning.Title);
}
[Fact]
public void ControlTemplate_Outputs_Error_When_Missing_TemplatePart_Nested_ItemTemplate_Case()
{
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:controls='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
TargetType='controls:CustomControlWithParts'>
<Border Name='PART_Typo_MainContentBorder'>
<StackPanel>
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- This PART_MainContentBorder shouldn't full parent ControlTemplate, PART_Typo_MainContentBorder still isn't properly named. -->
<Border Name='PART_MainContentBorder' />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ContentPresenter Name='PART_ContentPresenter'
Content='{TemplateBinding Content}'/>
</StackPanel>
</Border>
</ControlTemplate>";
var diagnostics = new List<RuntimeXamlDiagnostic>();
AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), new RuntimeXamlLoaderConfiguration
{
LocalAssembly = typeof(XamlIlTests).Assembly,
DiagnosticHandler = diagnostic =>
{
diagnostics.Add(diagnostic);
return diagnostic.Severity;
}
});
var warning = Assert.Single(diagnostics);
Assert.Equal(RuntimeXamlDiagnosticSeverity.Info, warning.Severity);
Assert.Contains("'PART_MainContentBorder'", warning.Title);
}
}
public class ListBoxHierarchyLine : Panel
{
@ -349,4 +449,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
set => SetValue(LineDashStyleProperty, value);
}
}
[TemplatePart("PART_MainContentBorder", typeof(Border))]
[TemplatePart("PART_ContentPresenter", typeof(ContentPresenter))]
public class CustomControlWithParts : ContentControl
{
}
public class CustomButtonWithParts : CustomControlWithParts
{
}
}

Loading…
Cancel
Save