From b57223eb1bd431da1eedddca629221ae476246c5 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Wed, 11 Feb 2026 02:53:07 -0800 Subject: [PATCH 1/4] Fix TabItem's Content inheriting TabControl's DataContext instead of TabItem's (#20541) * Child inherit DataContext from TabItem * Remove whitespace changes * Fix nullability error * Fix using Julien's solution --- src/Avalonia.Controls/TabControl.cs | 25 +++++- .../TabControlTests.cs | 77 +++++++++++++++---- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 396f82a622..dc9adc0fac 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Diagnostics; using Avalonia.Collections; using Avalonia.Automation.Peers; using Avalonia.Controls.Presenters; @@ -7,7 +7,6 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.VisualTree; using Avalonia.Automation; using Avalonia.Controls.Metadata; using Avalonia.Reactive; @@ -76,7 +75,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); + SelectedItemProperty.Changed.AddClassHandler((x, _) => x.UpdateSelectedContent()); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Tab); } @@ -231,7 +230,25 @@ namespace Avalonia.Controls } _selectedItemSubscriptions = new CompositeDisposable( - container.GetObservable(ContentControl.ContentProperty).Subscribe(v => SelectedContent = v), + container.GetObservable(ContentControl.ContentProperty).Subscribe(content => + { + var contentElement = content as StyledElement; + var contentDataContext = contentElement?.DataContext; + SelectedContent = content; + + // When the ContentPresenter (ContentPart) displays content that is a Control, it doesn't + // set its DataContext to that of the Control's. If the content doesn't set a DataContext, + // then it gets inherited from the TabControl. Work around this issue by setting the + // DataContext of the ContentPart to the content's original DataContext (inherited from + // container). + if (contentElement is not null && + contentElement.DataContext != contentDataContext && + ContentPart is not null) + { + Debug.Assert(!contentElement.IsSet(DataContextProperty)); + ContentPart.DataContext = contentDataContext; + } + }), container.GetObservable(ContentControl.ContentTemplateProperty).Subscribe(v => SelectedContentTemplate = SelectContentTemplate(v))); // Note how we fall back to our own ContentTemplate if the container doesn't specify one diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index af0fdd4348..3ea591d361 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -9,6 +9,8 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; +using Avalonia.Harfbuzz; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -259,51 +261,48 @@ namespace Avalonia.Controls.UnitTests [Fact] public void DataContexts_Should_Be_Correctly_Set() { + using var app = Start(); var items = new object[] { "Foo", new Item("Bar"), new TextBlock { Text = "Baz" }, new TabItem { Content = "Qux" }, - new TabItem { Content = new TextBlock { Text = "Bob" } } + new TabItem { Content = new TextBlock { Text = "Bob" } }, + new TabItem { DataContext = "Rob", Content = new TextBlock { Text = "Bob" } }, }; var target = new TabControl { - Template = TabControlTemplate(), DataContext = "Base", - DataTemplates = - { - new FuncDataTemplate((x, __) => new Button { Content = x }) - }, ItemsSource = items, }; - ApplyTemplate(target); + var root = CreateRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); - target.ContentPart!.UpdateChild(); - var dataContext = ((TextBlock)target.ContentPart.Child!).DataContext; + var dataContext = ((TextBlock)target.ContentPart!.Child!).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - target.ContentPart.UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - target.ContentPart.UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - target.ContentPart.UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - target.ContentPart.UpdateChild(); dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); + + target.SelectedIndex = 5; + dataContext = target.ContentPart.Child.DataContext; + Assert.Equal("Rob", dataContext); } /// @@ -843,6 +842,45 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static ControlTheme CreateTabControlControlTheme() + { + return new ControlTheme(typeof(TabControl)) + { + Setters = + { + new Setter(TabControl.TemplateProperty, TabControlTemplate()), + }, + }; + } + + private static ControlTheme CreateTabItemControlTheme() + { + return new ControlTheme(typeof(TabItem)) + { + Setters = + { + new Setter(TabItem.TemplateProperty, TabItemTemplate()), + }, + }; + } + + private static TestRoot CreateRoot(Control child) + { + return new TestRoot + { + Resources = + { + { typeof(TabControl), CreateTabControlControlTheme() }, + { typeof(TabItem), CreateTabItemControlTheme() }, + }, + DataTemplates = + { + new FuncDataTemplate((x, _) => new Button { Content = x.Value }) + }, + Child = child, + }; + } + private class TestTopLevel : TopLevel { private readonly ILayoutManager _layoutManager; @@ -892,6 +930,19 @@ namespace Avalonia.Controls.UnitTests target.ContentPart!.ApplyTemplate(); } + private IDisposable Start() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + fontManagerImpl: new HeadlessFontManagerStub(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: () => new KeyboardNavigationHandler(), + inputManager: new InputManager(), + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HarfBuzzTextShaper(), + assetLoader: new StandardAssetLoader())); + } + private class Item { public Item(string value) From a48c41502699db6084cd87d780606367cf965005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:03:49 +0000 Subject: [PATCH 2/4] FindAncestorOfType and FindDescendantOfType with predicates (#20661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Find ancestors/descendants with a predicate * Use Predicate instead of Func * Find ancestors/descendants with a predicate * Use Predicate instead of Func * Predicate to Predicate --------- Co-authored-by: Jan Kučera --- .../VisualTree/VisualExtensions.cs | 44 ++++++++++++++++--- .../VisualExtensionsTests.cs | 43 ++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index 55176a6502..b202a2e4b7 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -147,6 +147,19 @@ namespace Avalonia.VisualTree /// If given visual should be included in search. /// First ancestor of given type. public static T? FindAncestorOfType(this Visual? visual, bool includeSelf = false) where T : class + { + return FindAncestorOfType(visual, includeSelf, predicate: null); + } + + /// + /// Finds first ancestor of given type that matches a predicate. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// The predicate that the ancestor must match. + /// First ancestor of given type. + public static T? FindAncestorOfType(this Visual? visual, bool includeSelf, Predicate? predicate) where T : class { if (visual is null) { @@ -159,7 +172,10 @@ namespace Avalonia.VisualTree { if (parent is T result) { - return result; + if (predicate == null || predicate(result)) + { + return result; + } } parent = parent.VisualParent; @@ -176,18 +192,31 @@ namespace Avalonia.VisualTree /// If given visual should be included in search. /// First descendant of given type. public static T? FindDescendantOfType(this Visual? visual, bool includeSelf = false) where T : class + { + return FindDescendantOfType(visual, includeSelf, predicate: null); + } + + /// + /// Finds first descendant of given type that matches given predicate. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// The predicate that the descendant must match. + /// First descendant of given type that matches given predicate. + public static T? FindDescendantOfType(this Visual? visual, bool includeSelf, Predicate? predicate) where T : class { if (visual is null) { return null; } - if (includeSelf && visual is T result) + if (includeSelf && visual is T result && (predicate == null || predicate(result))) { return result; } - return FindDescendantOfTypeCore(visual); + return FindDescendantOfTypeCore(visual, predicate); } /// @@ -485,7 +514,7 @@ namespace Avalonia.VisualTree .Select(x => x.Element!); } - private static T? FindDescendantOfTypeCore(Visual visual) where T : class + private static T? FindDescendantOfTypeCore(Visual visual, Predicate? predicate) where T : class { var visualChildren = visual.VisualChildren; var visualChildrenCount = visualChildren.Count; @@ -496,10 +525,13 @@ namespace Avalonia.VisualTree if (child is T result) { - return result; + if (predicate == null || predicate(result)) + { + return result; + } } - var childResult = FindDescendantOfTypeCore(child); + var childResult = FindDescendantOfTypeCore(child, predicate); if (!(childResult is null)) { diff --git a/tests/Avalonia.Base.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Base.UnitTests/VisualExtensionsTests.cs index 4863c0fa61..270702f178 100644 --- a/tests/Avalonia.Base.UnitTests/VisualExtensionsTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualExtensionsTests.cs @@ -22,6 +22,23 @@ namespace Avalonia.Base.UnitTests Assert.Equal(root, target.FindAncestorOfType()); } + [Fact] + public void FindAncestorOfType_Finds_Visible_Parent() + { + StackPanel target; + + var root = new TestRoot + { + Child = new TestRoot + { + Child = target = new StackPanel(), + IsVisible = false + } + }; + + Assert.Equal(root, target.FindAncestorOfType(false, v => v.IsVisible)); + } + [Fact] public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child() { @@ -85,6 +102,32 @@ namespace Avalonia.Base.UnitTests Assert.Equal(target, root.FindDescendantOfType public static AutomationProperty HelpTextProperty { get; } = new AutomationProperty(); + /// + /// Identifies the itemStatus automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty ItemStatusProperty { get; } = new AutomationProperty(); + /// /// Identifies the landmark type automation property. The class name property value is returned /// by the method. diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 56111bdc83..b32b60118c 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -319,6 +319,41 @@ namespace Avalonia.Automation.Peers /// public int GetHeadingLevel() => GetHeadingLevelCore(); + + /// + /// Gets the item type that is associated with this automation peer. + /// + /// + /// + /// + /// Windows + /// UIA_ItemTypePropertyId + /// + /// + /// macOS + /// No mapping. + /// + /// + /// + public string? GetItemType() => GetItemTypeCore(); + + /// + /// Gets the item status that is associated with this automation peer. + /// + /// + /// + /// + /// Windows + /// UIA_ItemStatusPropertyId + /// + /// + /// macOS + /// No mapping. + /// + /// + /// + public string? GetItemStatus() => GetItemStatusCore(); + /// /// Gets the that is the parent of this . /// @@ -562,6 +597,8 @@ namespace Avalonia.Automation.Peers protected virtual string? GetHelpTextCore() => null; protected virtual AutomationLandmarkType? GetLandmarkTypeCore() => null; protected virtual int GetHeadingLevelCore() => 0; + protected virtual string? GetItemTypeCore() => null; + protected virtual string? GetItemStatusCore() => null; protected abstract AutomationPeer? GetParentCore(); protected abstract bool HasKeyboardFocusCore(); protected abstract bool IsContentElementCore(); diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index c59ba6b148..cdab4911f2 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -199,6 +199,8 @@ namespace Avalonia.Automation.Peers protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; protected override Rect GetBoundingRectangleCore() => GetBounds(Owner); protected override string GetClassNameCore() => Owner.GetType().Name; + protected override string? GetItemStatusCore() => AutomationProperties.GetItemStatus(Owner); + protected override string? GetItemTypeCore() => AutomationProperties.GetItemType(Owner); protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool IsContentElementCore() => true; protected override bool IsControlElementCore() => true; @@ -281,6 +283,13 @@ namespace Avalonia.Automation.Peers { InvalidateParent(); } + else if (e.Property == AutomationProperties.ItemStatusProperty) + { + RaisePropertyChangedEvent( + AutomationElementIdentifiers.ItemStatusProperty, + e.OldValue, + e.NewValue); + } } diff --git a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs index 8562807452..78f562decf 100644 --- a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs @@ -39,6 +39,7 @@ namespace Avalonia.Win32.Automation { AutomationElementIdentifiers.HelpTextProperty, UiaPropertyId.HelpText }, { AutomationElementIdentifiers.LandmarkTypeProperty, UiaPropertyId.LandmarkType }, { AutomationElementIdentifiers.HeadingLevelProperty, UiaPropertyId.HeadingLevel }, + { AutomationElementIdentifiers.ItemStatusProperty, UiaPropertyId.ItemStatus }, { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState }, { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly}, { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum }, @@ -135,6 +136,8 @@ namespace Avalonia.Win32.Automation UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()), UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()), UiaPropertyId.IsOffscreen => InvokeSync(() => Peer.IsOffscreen()), + UiaPropertyId.ItemType => InvokeSync(() => Peer.GetItemType()), + UiaPropertyId.ItemStatus => InvokeSync(() => Peer.GetItemStatus()), UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()), UiaPropertyId.Name => InvokeSync(() => Peer.GetName()), UiaPropertyId.HelpText => InvokeSync(() => Peer.GetHelpText()), From d9a6ae517fd59ffd2dbbc674e81d59a628c1458f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Feb 2026 18:36:04 +0100 Subject: [PATCH 4/4] Hide binding plugins and disable Data Annotations validation by default (#20623) * Make binding plugin API internal. Making everything pluggable like has performance implications even if the plugins are never customized (which will be the 99% case). `IPropertyAccessor` is used in `CompiledBindingPathBuilder` so needs to be kept public for the moment. * Disable Data Annotations validation by default. It [conflicts with `CommunityToolkit.Mvvm`](https://github.com/AvaloniaUI/Avalonia/issues/8397) which is now the default MVVM framework, so require it to be enabled in the `AppBuilder`. * Update API supressions. * Add back missing using --------- Co-authored-by: Julien Lebosquain --- api/Avalonia.nupkg.xml | 108 ++++++++++++++++++ .../Data/Core/Plugins/BindingPlugins.cs | 3 +- .../Data/Core/Plugins/DataValidationBase.cs | 2 +- .../Core/Plugins/ExceptionValidationPlugin.cs | 2 +- .../Core/Plugins/IDataValidationPlugin.cs | 2 +- .../Data/Core/Plugins/IPropertyAccessor.cs | 3 +- .../Core/Plugins/IPropertyAccessorPlugin.cs | 2 +- .../Data/Core/Plugins/IStreamPlugin.cs | 2 +- .../Core/Plugins/IndeiValidationPlugin.cs | 2 +- .../Data/Core/Plugins/PropertyAccessorBase.cs | 2 +- .../Data/Core/Plugins/PropertyError.cs | 2 +- src/Avalonia.Controls/AppBuilder.cs | 13 +++ .../BindingExpressionTests.DataValidation.cs | 5 + 13 files changed, 136 insertions(+), 12 deletions(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index b7efaf7869..e6a73822e9 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -25,6 +25,60 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Data.Core.Plugins.BindingPlugins + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.DataValidationBase + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.ExceptionValidationPlugin + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IDataValidationPlugin + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IndeiValidationPlugin + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IPropertyAccessorPlugin + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IStreamPlugin + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.PropertyAccessorBase + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.PropertyError + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Diagnostics.AppliedStyle @@ -253,6 +307,60 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Data.Core.Plugins.BindingPlugins + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.DataValidationBase + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.ExceptionValidationPlugin + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IDataValidationPlugin + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IndeiValidationPlugin + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IPropertyAccessorPlugin + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.IStreamPlugin + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.PropertyAccessorBase + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.Plugins.PropertyError + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Diagnostics.AppliedStyle diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs index 520c345ad5..50e137eac1 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// Holds a registry of plugins used for bindings. /// [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] - public static class BindingPlugins + internal static class BindingPlugins { internal static readonly List s_propertyAccessors = new() { @@ -17,7 +17,6 @@ namespace Avalonia.Data.Core.Plugins internal static readonly List s_dataValidators = new() { - new DataAnnotationsValidationPlugin(), new IndeiValidationPlugin(), new ExceptionValidationPlugin(), }; diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs index 6bef9f69f6..85ebf6ff78 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidationBase.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins /// and convert any values received from the inner property accessor into /// s. /// - public abstract class DataValidationBase : PropertyAccessorBase, IObserver + internal abstract class DataValidationBase : PropertyAccessorBase, IObserver { private readonly IPropertyAccessor _inner; diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 899b542f80..25c77e3282 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IDataValidationPlugin + internal class ExceptionValidationPlugin : IDataValidationPlugin { /// public bool Match(WeakReference reference, string memberName) => true; diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 5d2f06d2c6..339c8a656d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Defines how data validation is observed by an . /// - public interface IDataValidationPlugin + internal interface IDataValidationPlugin { /// /// Checks whether this plugin can handle data validation on the specified object. diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index 8941e86598..8db19bbffa 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs @@ -3,8 +3,7 @@ using System; namespace Avalonia.Data.Core.Plugins { /// - /// Defines an accessor to a property on an object returned by a - /// + /// Defines an accessor to a property on an object. /// public interface IPropertyAccessor : IDisposable { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs index e2d7312ac4..fb24969c7e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Defines how a member is read, written and observed by a binding. /// - public interface IPropertyAccessorPlugin + internal interface IPropertyAccessorPlugin { /// /// Checks whether this plugin can handle accessing the properties of the specified object. diff --git a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs index b741cfaca2..da86543507 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Defines a plugin that handles the '^' stream binding operator. /// - public interface IStreamPlugin + internal interface IStreamPlugin { /// /// Checks whether this plugin handles the specified value. diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 385d96a7b8..a06514b5da 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -9,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IDataValidationPlugin + internal class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index df36347e93..ad4e46ca08 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Defines a default base implementation for a . /// - public abstract class PropertyAccessorBase : IPropertyAccessor + internal abstract class PropertyAccessorBase : IPropertyAccessor { private Action? _listener; diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs index 7143a4269a..dae52e8541 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core.Plugins /// /// An that represents an error. /// - public class PropertyError : IPropertyAccessor + internal class PropertyError : IPropertyAccessor { private readonly BindingNotification _error; diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 9d6bcff813..9259be8594 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -1,10 +1,12 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Data.Core.Plugins; namespace Avalonia { @@ -265,6 +267,17 @@ namespace Avalonia return Self; } + /// + /// Adds support for validation using System.ComponentModel.DataAnnotations. + /// + [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] + public AppBuilder WithDataAnnotationsValidation() + { + if (!BindingPlugins.DataValidators.Any(x => x is DataAnnotationsValidationPlugin)) + BindingPlugins.DataValidators.Insert(0, new DataAnnotationsValidationPlugin()); + return Self; + } + /// /// Registers an action that is executed with the current font manager. /// diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs index 8dd75fedf0..04aa7b8de2 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using Avalonia.Data; +using Avalonia.Data.Core.Plugins; using Avalonia.UnitTests; using Xunit; @@ -273,6 +275,9 @@ public partial class BindingExpressionTests [Fact] public void Updates_Data_Validation_For_Required_DataAnnotation() { + if (!BindingPlugins.DataValidators.Any(x => x is DataAnnotationsValidationPlugin)) + BindingPlugins.DataValidators.Insert(0, new DataAnnotationsValidationPlugin()); + var data = new DataAnnotationsViewModel(); var target = CreateTargetWithSource( data,