diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs
index 34f3a76b61..9512dc34df 100644
--- a/src/Avalonia.Base/Styling/ChildSelector.cs
+++ b/src/Avalonia.Base/Styling/ChildSelector.cs
@@ -65,6 +65,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs
index 4ffaff6861..677a924189 100644
--- a/src/Avalonia.Base/Styling/DescendentSelector.cs
+++ b/src/Avalonia.Base/Styling/DescendentSelector.cs
@@ -70,6 +70,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs
index 4393d3239f..77c5b719c6 100644
--- a/src/Avalonia.Base/Styling/NestingSelector.cs
+++ b/src/Avalonia.Base/Styling/NestingSelector.cs
@@ -33,6 +33,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => true;
+ protected override Selector? MovePreviousOrParent() => null;
}
}
diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs
index 76a0690e96..c7727bb6b8 100644
--- a/src/Avalonia.Base/Styling/NotSelector.cs
+++ b/src/Avalonia.Base/Styling/NotSelector.cs
@@ -67,6 +67,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
}
}
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index 047bf434da..f473791664 100644
--- a/src/Avalonia.Base/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Base/Styling/NthChildSelector.cs
@@ -105,7 +105,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
public override string ToString()
{
diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs
index 913c27bf0c..af9249864f 100644
--- a/src/Avalonia.Base/Styling/OrSelector.cs
+++ b/src/Avalonia.Base/Styling/OrSelector.cs
@@ -103,18 +103,12 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
+ protected override Selector? MovePreviousOrParent() => null;
- internal override bool HasValidNestingSelector()
+ internal override void ValidateNestingSelector(bool inControlTheme)
{
foreach (var selector in _selectors)
- {
- if (!selector.HasValidNestingSelector())
- {
- return false;
- }
- }
-
- return true;
+ selector.ValidateNestingSelector(inControlTheme);
}
private Type? EvaluateTargetType()
diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
index 7a37daf087..48136ba2de 100644
--- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
+++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
@@ -90,7 +90,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{
diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs
index 1e06f3d375..7ce17518dd 100644
--- a/src/Avalonia.Base/Styling/Selector.cs
+++ b/src/Avalonia.Base/Styling/Selector.cs
@@ -86,7 +86,36 @@ namespace Avalonia.Styling
///
protected abstract Selector? MovePrevious();
- internal abstract bool HasValidNestingSelector();
+ ///
+ /// Moves to the previous selector or the parent selector.
+ ///
+ protected abstract Selector? MovePreviousOrParent();
+
+ internal virtual void ValidateNestingSelector(bool inControlTheme)
+ {
+ var s = this;
+ var templateCount = 0;
+
+ do
+ {
+ if (inControlTheme)
+ {
+ if (!s.InTemplate && s.IsCombinator)
+ throw new InvalidOperationException(
+ "ControlTheme style may not directly contain a child or descendent selector.");
+ if (s is TemplateSelector && templateCount++ > 0)
+ throw new InvalidOperationException(
+ "ControlTemplate styles cannot contain multiple template selectors.");
+ }
+
+ var previous = s.MovePreviousOrParent();
+
+ if (previous is null && s is not NestingSelector)
+ throw new InvalidOperationException("Child styles must have a nesting selector.");
+
+ s = previous;
+ } while (s is not null);
+ }
private static SelectorMatch MatchUntilCombinator(
IStyleable control,
diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs
index 7a6b746488..77c4e62d29 100644
--- a/src/Avalonia.Base/Styling/Style.cs
+++ b/src/Avalonia.Base/Styling/Style.cs
@@ -77,8 +77,13 @@ namespace Avalonia.Styling
{
if (Selector is null)
throw new InvalidOperationException("Child styles must have a selector.");
- if (!Selector.HasValidNestingSelector())
- throw new InvalidOperationException("Child styles must have a nesting selector.");
+ Selector.ValidateNestingSelector(false);
+ }
+ else if (parent is ControlTheme)
+ {
+ if (Selector is null)
+ throw new InvalidOperationException("Child styles must have a selector.");
+ Selector.ValidateNestingSelector(true);
}
base.SetParent(parent);
diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs
index 4c011f1b0d..3a27275438 100644
--- a/src/Avalonia.Base/Styling/Styles.cs
+++ b/src/Avalonia.Base/Styling/Styles.cs
@@ -26,6 +26,11 @@ namespace Avalonia.Styling
{
_styles.ResetBehavior = ResetBehavior.Remove;
_styles.CollectionChanged += OnCollectionChanged;
+ _styles.Validate = i =>
+ {
+ if (i is ControlTheme)
+ throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection.");
+ };
}
public Styles(IResourceHost owner)
diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs
index b0a2dae8d6..278e24a203 100644
--- a/src/Avalonia.Base/Styling/TemplateSelector.cs
+++ b/src/Avalonia.Base/Styling/TemplateSelector.cs
@@ -49,6 +49,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
- internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _parent;
}
}
diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
index 24d5d6bbbf..6681a7da36 100644
--- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
+++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
@@ -140,7 +140,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
- internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
+ protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString()
{
diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
index 93a0e6c2fd..7a27a02fc4 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
@@ -1,5 +1,6 @@
using System;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Xunit;
@@ -7,6 +8,15 @@ namespace Avalonia.Base.UnitTests.Styling
{
public class ControlThemeTests
{
+ [Fact]
+ public void ControlTheme_Cannot_Be_Added_To_Styles()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var styles = new Styles();
+
+ Assert.Throws(() => styles.Add(target));
+ }
+
[Fact]
public void ControlTheme_Cannot_Be_Added_To_Style_Children()
{
@@ -24,5 +34,59 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Throws(() => other.Children.Add(target));
}
+
+ [Fact]
+ public void Style_Without_Selector_Cannot_Be_Added_To_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var child = new Style();
+
+ Assert.Throws(() => target.Children.Add(child));
+ }
+
+ [Fact]
+ public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children()
+ {
+ var target = new ControlTheme(typeof(Button));
+ var child = new Style(x => x.OfType