Skip to content

Commit 68009f8

Browse files
authored
Warning AVLN2208: Item container in the data template (#18132)
* Implement AVLN2208 diagnostic - ItemContainerInsideTemplate * Add AVLN2208 tests * Enable AVLN2208 as an error in the repository globally * Fix invalid ListBoxItem inside of DataTemplate in devtools
1 parent fa14f9e commit 68009f8

File tree

9 files changed

+199
-39
lines changed

9 files changed

+199
-39
lines changed

.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ avalonia_xaml_diagnostic.AVLN2205.severity = error
220220
avalonia_xaml_diagnostic.AVLN2206.severity = info
221221
# TemplatePartWrongType
222222
avalonia_xaml_diagnostic.AVLN2207.severity = error
223+
# ItemContainerInsideTemplate
224+
avalonia_xaml_diagnostic.AVLN2208.severity = error
223225
# Obsolete
224226
avalonia_xaml_diagnostic.AVLN5001.severity = error
225227

build/WarnAsErrors.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@
1717
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2205</WarningsNotAsErrors>
1818
<!-- AVLN2207: TemplatePartWrongType -->
1919
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2207</WarningsNotAsErrors>
20+
<!-- AVLN2208: ItemContainerInsideTemplate -->
21+
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2208</WarningsNotAsErrors>
2022
</PropertyGroup>
2123
</Project>

src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:vm="using:Avalonia.Diagnostics.ViewModels"
44
xmlns:controls="using:Avalonia.Diagnostics.Controls"
5+
xmlns:models="using:Avalonia.Diagnostics.Models"
56
x:Class="Avalonia.Diagnostics.Views.EventsPageView"
67
Margin="2"
78
x:DataType="vm:EventsPageViewModel">
@@ -73,34 +74,37 @@
7374

7475
<ListBox Name="EventsList" ItemsSource="{Binding RecordedEvents}"
7576
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
77+
<ListBox.Styles>
78+
<Style Selector="ListBoxItem">
79+
<Setter Property="Classes.handled" Value="{Binding IsHandled, DataType=vm:FiredEvent}" />
80+
</Style>
81+
</ListBox.Styles>
7682

7783
<ListBox.ItemTemplate>
7884
<DataTemplate>
79-
<ListBoxItem Classes.handled="{Binding IsHandled}">
80-
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto">
85+
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto">
8186

82-
<TextBlock Grid.Column="0" Text="{Binding TriggerTime, StringFormat={}{0:HH:mm:ss.fff}}"/>
87+
<TextBlock Grid.Column="0" Text="{Binding TriggerTime, StringFormat={}{0:HH:mm:ss.fff}}"/>
8388

84-
<StackPanel Margin="10,0,0,0" Grid.Column="1" Spacing="2" Orientation="Horizontal" >
85-
<TextBlock Tag="{Binding Event}" DoubleTapped="NavigateTo" Text="{Binding Event.Name}" FontWeight="Bold" Classes="nav" />
86-
<TextBlock Text="on" />
87-
<TextBlock Tag="{Binding Originator}" DoubleTapped="NavigateTo" Text="{Binding Originator.HandlerName}" Classes="nav" />
88-
</StackPanel>
89+
<StackPanel Margin="10,0,0,0" Grid.Column="1" Spacing="2" Orientation="Horizontal" >
90+
<TextBlock Tag="{Binding Event}" DoubleTapped="NavigateTo" Text="{Binding Event.Name}" FontWeight="Bold" Classes="nav" />
91+
<TextBlock Text="on" />
92+
<TextBlock Tag="{Binding Originator}" DoubleTapped="NavigateTo" Text="{Binding Originator.HandlerName}" Classes="nav" />
93+
</StackPanel>
8994

90-
<StackPanel Margin="2,0,0,0" Grid.Column="2" Spacing="2" Orientation="Horizontal" IsVisible="{Binding IsHandled}" >
91-
<TextBlock Text="::" />
92-
<TextBlock Text="Handled by" />
93-
<TextBlock Tag="{Binding HandledBy}" DoubleTapped="NavigateTo" Text="{Binding HandledBy.HandlerName}" Classes="nav" />
94-
</StackPanel>
95+
<StackPanel Margin="2,0,0,0" Grid.Column="2" Spacing="2" Orientation="Horizontal" IsVisible="{Binding IsHandled}" >
96+
<TextBlock Text="::" />
97+
<TextBlock Text="Handled by" />
98+
<TextBlock Tag="{Binding HandledBy}" DoubleTapped="NavigateTo" Text="{Binding HandledBy.HandlerName}" Classes="nav" />
99+
</StackPanel>
95100

96-
<StackPanel Grid.Column="4" Orientation="Horizontal" HorizontalAlignment="Right">
97-
<TextBlock Text="Routing (" />
98-
<TextBlock Text="{Binding Event.RoutingStrategies}"/>
99-
<TextBlock Text=")"/>
100-
</StackPanel>
101+
<StackPanel Grid.Column="4" Orientation="Horizontal" HorizontalAlignment="Right">
102+
<TextBlock Text="Routing (" />
103+
<TextBlock Text="{Binding Event.RoutingStrategies}"/>
104+
<TextBlock Text=")"/>
105+
</StackPanel>
101106

102-
</Grid>
103-
</ListBoxItem>
107+
</Grid>
104108
</DataTemplate>
105109
</ListBox.ItemTemplate>
106110
</ListBox>
@@ -111,26 +115,29 @@
111115
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
112116

113117
<ListBox ItemsSource="{Binding SelectedEvent.EventChain}">
118+
<ListBox.Styles>
119+
<Style Selector="ListBoxItem">
120+
<Setter Property="Classes.handled" Value="{Binding Handled, DataType=models:EventChainLink}" />
121+
</Style>
122+
</ListBox.Styles>
114123
<ListBox.ItemTemplate>
115124
<DataTemplate>
116-
<ListBoxItem Classes.handled="{Binding Handled}"
117-
PointerEntered="ListBoxItem_PointerEntered"
118-
PointerExited="ListBoxItem_PointerExited"
119-
>
120-
<StackPanel Orientation="Vertical">
121-
122-
<Rectangle IsVisible="{Binding BeginsNewRoute}" StrokeDashArray="2,2" StrokeThickness="1" Stroke="Gray" />
123-
124-
<StackPanel Orientation="Horizontal" Spacing="2">
125-
<TextBlock Text="{Binding Route}" FontWeight="Bold" />
126-
<TextBlock Tag="{Binding}"
127-
DoubleTapped="NavigateTo"
128-
Text="{Binding HandlerName}"
129-
Classes="nav" />
130-
</StackPanel>
131-
125+
<StackPanel Orientation="Vertical"
126+
Background="Transparent"
127+
PointerEntered="ListBoxItem_PointerEntered"
128+
PointerExited="ListBoxItem_PointerExited">
129+
130+
<Rectangle IsVisible="{Binding BeginsNewRoute}" StrokeDashArray="2,2" StrokeThickness="1" Stroke="Gray" />
131+
132+
<StackPanel Orientation="Horizontal" Spacing="2">
133+
<TextBlock Text="{Binding Route}" FontWeight="Bold" />
134+
<TextBlock Tag="{Binding}"
135+
DoubleTapped="NavigateTo"
136+
Text="{Binding HandlerName}"
137+
Classes="nav" />
132138
</StackPanel>
133-
</ListBoxItem>
139+
140+
</StackPanel>
134141
</DataTemplate>
135142
</ListBox.ItemTemplate>
136143
</ListBox>

src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private void InitializeComponent()
7777
private void ListBoxItem_PointerEntered(object? sender, PointerEventArgs e)
7878
{
7979
if (DataContext is EventsPageViewModel vm
80-
&& sender is ListBoxItem control
80+
&& sender is Control control
8181
&& control.DataContext is EventChainLink chainLink
8282
&& chainLink.Handler is Visual visual)
8383
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal static class AvaloniaXamlDiagnosticCodes
2828
public const string RequiredTemplatePartMissing = "AVLN2205";
2929
public const string OptionalTemplatePartMissing = "AVLN2206";
3030
public const string TemplatePartWrongType = "AVLN2207";
31+
public const string ItemContainerInsideTemplate = "AVLN2208";
3132

3233
// XAML emit errors 3000-3999.
3334
public const string EmitError = "AVLN3000";

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ void InsertBeforeMany(Type[] types, params IXamlAstTransformer[] t)
6666
new AvaloniaXamlIlConstructorServiceProviderTransformer(),
6767
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
6868
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(),
69-
new AvaloniaXamlIlThemeVariantProviderTransformer()
69+
new AvaloniaXamlIlThemeVariantProviderTransformer(),
70+
new AvaloniaXamlIlDataTemplateWarningsTransformer()
7071
);
7172
InsertBefore<ConvertPropertyValuesToAssignmentsTransformer>(
7273
new AvaloniaXamlIlOptionMarkupExtensionTransformer());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
6+
using XamlX;
7+
using XamlX.Ast;
8+
using XamlX.Transform;
9+
using XamlX.TypeSystem;
10+
11+
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
12+
13+
#if !XAMLX_INTERNAL
14+
public
15+
#endif
16+
class AvaloniaXamlIlDataTemplateWarningsTransformer : IXamlAstTransformer
17+
{
18+
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
19+
{
20+
var avaloniaTypes = context.GetAvaloniaTypes();
21+
var contentControl = context.GetAvaloniaTypes().ContentControl;
22+
23+
// This transformers only looks for ContentControl delivered objects inside of DataTemplate
24+
if ((node is not XamlAstObjectNode objectNode)
25+
|| !contentControl.IsAssignableFrom(objectNode.Type.GetClrType())
26+
|| context.ParentNodes().FirstOrDefault() is not XamlAstObjectNode parentNode
27+
|| !avaloniaTypes.IDataTemplate.IsAssignableFrom(parentNode.Type.GetClrType()))
28+
{
29+
return node;
30+
}
31+
32+
// And only inside of ItemTemplate or DataTemplates property value.
33+
if (context.ParentNodes().OfType<XamlAstXamlPropertyValueNode>().FirstOrDefault() is not { } valueNode
34+
|| valueNode.Property.GetClrProperty() is not { } clrProperty
35+
|| !((clrProperty.Name == "ItemTemplate" && clrProperty.DeclaringType == avaloniaTypes.ItemsControl)
36+
|| (clrProperty.Name == "DataTemplates" && clrProperty.DeclaringType == avaloniaTypes.Control)))
37+
{
38+
return node;
39+
}
40+
41+
// And only inside of ItemsControl
42+
if (context.ParentNodes().SkipWhile(p => p != valueNode)
43+
.OfType<XamlAstObjectNode>().FirstOrDefault() is not { } itemsControlNode
44+
|| !avaloniaTypes.ItemsControl.IsAssignableFrom(itemsControlNode.Type.GetClrType()))
45+
{
46+
return node;
47+
}
48+
49+
// Avalonia doesn't have any reliable way to determine container type from the API.
50+
if (GetKnownItemContainerTypeFullName(itemsControlNode.Type.GetClrType()) is not { } knownItemContainerTypeName
51+
|| itemsControlNode.Type.GetClrType().Assembly?.FindType(knownItemContainerTypeName) is not { } knownItemContainerType)
52+
{
53+
return node;
54+
}
55+
56+
if (knownItemContainerType.IsAssignableFrom(objectNode.Type.GetClrType()))
57+
{
58+
context.ReportDiagnostic(new XamlDiagnostic(
59+
AvaloniaXamlDiagnosticCodes.ItemContainerInsideTemplate,
60+
XamlDiagnosticSeverity.Warning,
61+
$"Unexpected '{knownItemContainerType.Name}' inside of '{itemsControlNode.Type.GetClrType().Name}.{clrProperty.Name}'. "
62+
+ $"'{itemsControlNode.Type.GetClrType().Name}.{clrProperty.Name}' defines template of the container content, not the container itself.", node));
63+
}
64+
65+
return node;
66+
}
67+
68+
private static string? GetKnownItemContainerTypeFullName(IXamlType itemsControlType) => itemsControlType.FullName switch
69+
{
70+
"Avalonia.Controls.ListBox" => "Avalonia.Controls.ListBoxItem",
71+
"Avalonia.Controls.ComboBox" => "Avalonia.Controls.ComboBoxItem",
72+
"Avalonia.Controls.Menu" => "Avalonia.Controls.MenuItem",
73+
"Avalonia.Controls.MenuItem" => "Avalonia.Controls.MenuItem",
74+
"Avalonia.Controls.Primitives.TabStrip" => "Avalonia.Controls.Primitives.TabStripItem",
75+
"Avalonia.Controls.TabControl" => "Avalonia.Controls.TabItem",
76+
"Avalonia.Controls.TreeView" => "Avalonia.Controls.TreeViewItem",
77+
_ => null
78+
};
79+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ sealed class AvaloniaXamlIlWellKnownTypes
6363
public IXamlType IDataTemplate { get; }
6464
public IXamlType ITemplateOfControl { get; }
6565
public IXamlType Control { get; }
66+
public IXamlType ContentControl { get; }
6667
public IXamlType ItemsControl { get; }
6768
public IXamlType ReflectionBindingExtension { get; }
6869

@@ -249,6 +250,7 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
249250
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
250251
IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate");
251252
Control = cfg.TypeSystem.GetType("Avalonia.Controls.Control");
253+
ContentControl = cfg.TypeSystem.GetType("Avalonia.Controls.ContentControl");
252254
ITemplateOfControl = cfg.TypeSystem.GetType("Avalonia.Controls.ITemplate`1").MakeGenericType(Control);
253255
ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl");
254256
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");

tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,72 @@ public void Control_Theme_Parser_Throws_For_Duplicate_Setter()
392392
Assert.Equal(RuntimeXamlDiagnosticSeverity.Warning, warning.Severity);
393393
Assert.StartsWith("Duplicate setter encountered for property 'Height'", warning.Title);
394394
}
395+
396+
[Fact]
397+
public void Item_Container_Inside_Of_ItemTemplate_Should_Be_Warned()
398+
{
399+
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
400+
401+
var xaml = new RuntimeXamlLoaderDocument(@"
402+
<ListBox xmlns='https://github.com/avaloniaui'
403+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
404+
<ListBox.ItemTemplate>
405+
<DataTemplate>
406+
<ListBoxItem />
407+
</DataTemplate>
408+
</ListBox.ItemTemplate>
409+
</ListBox>");
410+
var diagnostics = new List<RuntimeXamlDiagnostic>();
411+
// We still have a runtime check in the StyleInstance class, but in this test we only care about compile warnings.
412+
var listBox = (ListBox)AvaloniaRuntimeXamlLoader.Load(xaml, new RuntimeXamlLoaderConfiguration
413+
{
414+
DiagnosticHandler = diagnostic =>
415+
{
416+
diagnostics.Add(diagnostic);
417+
return diagnostic.Severity;
418+
}
419+
});
420+
// ItemTemplate should still work as before, creating whatever object user put inside
421+
Assert.IsType<ListBoxItem>(listBox.ItemTemplate!.Build(null));
422+
423+
// But invalid usage should be warned:
424+
var warning = Assert.Single(diagnostics);
425+
Assert.Equal(RuntimeXamlDiagnosticSeverity.Warning, warning.Severity);
426+
Assert.Equal("AVLN2208", warning.Id);
427+
}
428+
429+
[Fact]
430+
public void Item_Container_Inside_Of_DataTemplates_Should_Be_Warned()
431+
{
432+
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
433+
434+
var xaml = new RuntimeXamlLoaderDocument(@"
435+
<TabControl xmlns='https://github.com/avaloniaui'
436+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
437+
<TabControl.DataTemplates>
438+
<DataTemplate x:DataType='x:Object'>
439+
<TabItem />
440+
</DataTemplate>
441+
</TabControl.DataTemplates>
442+
</TabControl>");
443+
var diagnostics = new List<RuntimeXamlDiagnostic>();
444+
// We still have a runtime check in the StyleInstance class, but in this test we only care about compile warnings.
445+
var tabControl = (TabControl)AvaloniaRuntimeXamlLoader.Load(xaml, new RuntimeXamlLoaderConfiguration
446+
{
447+
DiagnosticHandler = diagnostic =>
448+
{
449+
diagnostics.Add(diagnostic);
450+
return diagnostic.Severity;
451+
}
452+
});
453+
// ItemTemplate should still work as before, creating whatever object user put inside
454+
Assert.IsType<TabItem>(tabControl.DataTemplates[0]!.Build(null));
455+
456+
// But invalid usage should be warned:
457+
var warning = Assert.Single(diagnostics);
458+
Assert.Equal(RuntimeXamlDiagnosticSeverity.Warning, warning.Severity);
459+
Assert.Equal("AVLN2208", warning.Id);
460+
}
395461
}
396462

397463
public class XamlIlBugTestsEventHandlerCodeBehind : Window

0 commit comments

Comments
 (0)