From 5bfbfb97d3689acd353136b05136f8d825010ef6 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sun, 14 Feb 2021 11:36:06 +0300 Subject: [PATCH 01/35] Use ControlCatalog.NetCore as an artifact instead of ControlCatalog.Desktop --- nukebuild/Build.cs | 17 +++++++++++------ nukebuild/BuildParameters.cs | 7 ++----- .../ControlCatalog.NetCore.csproj | 6 +++++- samples/ControlCatalog/ControlCatalog.csproj | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8e331edab4..2c6ec70bd6 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -305,14 +305,19 @@ partial class Build : NukeBuild .Executes(() => { var data = Parameters; + var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore"; + var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish"; + + DotNetPublish(c => c + .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj") + .EnableNoBuild() + .SetConfiguration(data.Configuration) + .AddProperty("PackageVersion", data.Version) + .AddProperty("PublishDir", pathToPublish)); + Zip(data.ZipCoreArtifacts, data.BinRoot); Zip(data.ZipNuGetArtifacts, data.NugetRoot); - Zip(data.ZipTargetControlCatalogDesktopDir, - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe"))); + Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish); }); Target CreateIntermediateNugetPackages => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index c76019d9eb..a92c988fbd 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -58,8 +58,7 @@ public partial class Build public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } - public AbsolutePath ZipSourceControlCatalogDesktopDir { get; } - public AbsolutePath ZipTargetControlCatalogDesktopDir { get; } + public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; } public BuildParameters(Build b) @@ -129,9 +128,7 @@ public partial class Build FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix); - ZipSourceControlCatalogDesktopDir = - RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461"); - ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix); + ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix); } string GetVersion() diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d5aedf7783..3c2d2ee359 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -1,7 +1,7 @@  - Exe + WinExe netcoreapp3.1 true @@ -15,6 +15,10 @@ + + + en + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 1aa926a2a6..53ad213d92 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -27,6 +27,6 @@ - + From d7f3c24365a9cb73649363892ea0296e89b1a9c1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 13:24:20 +0100 Subject: [PATCH 02/35] Allow for controlling behavior of CaptionButtons. --- .../Chrome/CaptionButtons.cs | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index cd60130c5b..d41a95b5a0 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -14,17 +14,21 @@ namespace Avalonia.Controls.Chrome public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; - private Window? _hostWindow; - public void Attach(Window hostWindow) + /// + /// Currently attached window. + /// + protected Window? HostWindow { get; private set; } + + public virtual void Attach(Window hostWindow) { if (_disposables == null) { - _hostWindow = hostWindow; + HostWindow = hostWindow; _disposables = new CompositeDisposable { - _hostWindow.GetObservable(Window.WindowStateProperty) + HostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { PseudoClasses.Set(":minimized", x == WindowState.Minimized); @@ -36,14 +40,45 @@ namespace Avalonia.Controls.Chrome } } - public void Detach() + public virtual void Detach() { if (_disposables != null) { _disposables.Dispose(); _disposables = null; - _hostWindow = null; + HostWindow = null; + } + } + + protected virtual void OnClose() + { + HostWindow?.Close(); + } + + protected virtual void OnRestore() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + protected virtual void OnMinimize() + { + if (HostWindow != null) + { + HostWindow.WindowState = WindowState.Minimized; + } + } + + private void OnToggleFullScreen() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen + ? WindowState.Normal + : WindowState.FullScreen; } } @@ -56,31 +91,13 @@ namespace Avalonia.Controls.Chrome var minimiseButton = e.NameScope.Get("PART_MinimiseButton"); var fullScreenButton = e.NameScope.Get("PART_FullScreenButton"); - closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); + closeButton.PointerReleased += (sender, e) => OnClose(); - restoreButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - }; + restoreButton.PointerReleased += (sender, e) => OnRestore(); - minimiseButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = WindowState.Minimized; - } - }; + minimiseButton.PointerReleased += (sender, e) => OnMinimize(); - fullScreenButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; - } - }; + fullScreenButton.PointerReleased += (sender, e) => OnToggleFullScreen(); } } } From 8a79141c24647376e392790607170ba3b4a31aa5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 14:56:51 +0100 Subject: [PATCH 03/35] Fix access modifer. --- src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index d41a95b5a0..be15d3d444 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Chrome } } - private void OnToggleFullScreen() + protected virtual void OnToggleFullScreen() { if (HostWindow != null) { From cf8117d9ecfe53687879ee4c6af1223f78d8319d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 17:58:08 +0000 Subject: [PATCH 04/35] change default extend chrome hint --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 ++- src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs | 2 +- src/Avalonia.Native/avn.idl | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index e5adc8c6ed..f55f440db9 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,6 +1,7 @@ Compat issues with assembly Avalonia.Controls: MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 4 +Total Issues: 5 diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index de3f58886b..bb3c0288eb 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -16,7 +16,7 @@ namespace Avalonia.Platform /// /// The default for the platform. /// - Default = SystemChrome, + Default = PreferSystemChrome, /// /// Use SystemChrome diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 57a0c32067..476e64bd2d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints AvnSystemChrome = 0x01, AvnPreferSystemChrome = 0x02, AvnOSXThickTitleBar = 0x08, - AvnDefaultChrome = AvnSystemChrome, + AvnDefaultChrome = AvnPreferSystemChrome, } [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] From 899ba649130a4327cd2d755e3c6ea76b10ddda60 Mon Sep 17 00:00:00 2001 From: aljosas Date: Fri, 12 Mar 2021 12:15:12 +0100 Subject: [PATCH 05/35] fixing data validation for Combobox control and all SelectingItems control #5652 --- .../Primitives/SelectingItemsControl.cs | 28 +++-- .../CarouselTests.cs | 110 +++++++----------- .../ComboBoxTests.cs | 65 +++++++++++ .../ListBoxTests.cs | 7 ++ 4 files changed, 136 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2cd69793dc..ccdff8c9b7 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives EndUpdating(); } + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new binding value for the property. + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + { + if (property == SelectedItemProperty) + { + DataValidationErrors.SetError(this, value.Error); + } + } + protected override void OnInitialized() { base.OnInitialized(); @@ -503,6 +517,7 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } + if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -707,7 +722,7 @@ namespace Avalonia.Controls.Primitives _oldSelectedItem = SelectedItem; } else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && - _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) + _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { RaisePropertyChanged( SelectedItemsProperty, @@ -853,10 +868,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new InternalSelectionModel - { - SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), - }; + return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; } private void InitializeSelectionModel(ISelectionModel model) @@ -977,7 +989,7 @@ namespace Avalonia.Controls.Primitives public Optional Selection { get; set; } public Optional SelectedItems { get; set; } - public Optional SelectedIndex + public Optional SelectedIndex { get => _selectedIndex; set @@ -996,6 +1008,6 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } - } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 051f6c3fd3..393fde0faf 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -1,10 +1,14 @@ using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; +using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -16,12 +20,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -35,12 +34,7 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = new[] - { - "Foo", - "Bar" - } + Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } }; target.ApplyTemplate(); @@ -75,18 +69,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -111,18 +98,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = true, + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = true, }; target.ApplyTemplate(); @@ -150,9 +130,7 @@ namespace Avalonia.Controls.UnitTests var items = new ObservableCollection(); var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -170,18 +148,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -204,12 +175,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { @@ -233,18 +199,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -267,18 +226,11 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "FooBar" - }; + var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), - Items = items, - IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false }; target.ApplyTemplate(); @@ -311,5 +263,31 @@ namespace Avalonia.Controls.UnitTests contentPresenter.UpdateChild(); return Assert.IsType(contentPresenter.Child); } + + [Fact] + public void SelectedItem_Validation() + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = + new BehaviorSubject(new BindingNotification(exception, + BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 4ea838358c..9f90037032 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -1,11 +1,14 @@ using System.Linq; +using System.Reactive.Subjects; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -173,5 +176,67 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(expectedSelectedIndex, target.SelectedIndex); } } + + [Fact] + public void SelectedItem_Validation() + { + + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new ComboBox + { + Template = GetTemplate() + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + + } + + } + private ComboBox CreateControl() + { + var control = new ComboBox() + { + Template = GetTemplate() + }; + + control.ApplyTemplate(); + return control; + } + + private TextBox GetTextBox(ComboBox control) + { + return control.GetTemplateChildren() + // .OfType() + // .Select(b => b.Content) + .OfType() + .First(); + } + // private IControlTemplate CreateTemplate() + // { + // return new FuncControlTemplate((control, scope) => + // { + // var textBox = + // new TextBox + // { + // Name = "PART_TextBox" + // }.RegisterInNameScope(scope); + // return new ButtonSpinner + // { + // Name = "PART_Spinner", + // Content = textBox, + // }.RegisterInNameScope(scope); + // }); + // } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 145fce4fed..d13f5b704d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -559,5 +559,12 @@ namespace Avalonia.Controls.UnitTests public string Value { get; } } + + + [Fact] + public void SelectedItem_Validation() + { + + } } } From ede1e2db94b250af9364f3eb197738335c577d9e Mon Sep 17 00:00:00 2001 From: aljosas Date: Mon, 15 Mar 2021 13:01:42 +0100 Subject: [PATCH 06/35] fixing PR for data validation for Combobox control and all SelectingItemsControl --- .../Primitives/SelectingItemsControl.cs | 6 +- .../CarouselTests.cs | 80 +++++++++++++++---- .../ComboBoxTests.cs | 37 +-------- .../ListBoxTests.cs | 19 +++++ 4 files changed, 90 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ccdff8c9b7..19824a71f0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -517,7 +517,6 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } - if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); @@ -868,7 +867,10 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; + return new InternalSelectionModel + { + SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), + }; } private void InitializeSelectionModel(ISelectionModel model) diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 393fde0faf..ea39093602 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -20,7 +20,12 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } + Template = new FuncControlTemplate(CreateTemplate), + Items = new[] + { + "Foo", + "Bar" + } }; target.ApplyTemplate(); @@ -34,7 +39,12 @@ namespace Avalonia.Controls.UnitTests { var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = new[] { "Foo", "Bar" } + Template = new FuncControlTemplate(CreateTemplate), + Items = new[] + { + "Foo", + "Bar" + } }; target.ApplyTemplate(); @@ -69,11 +79,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -98,11 +115,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = true, + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = true, }; target.ApplyTemplate(); @@ -130,7 +154,9 @@ namespace Avalonia.Controls.UnitTests var items = new ObservableCollection(); var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -148,11 +174,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Changes_To_None_When_Items_Assigned_Null() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -175,7 +208,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { @@ -199,11 +237,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); @@ -226,11 +271,18 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { - var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; var target = new Carousel { - Template = new FuncControlTemplate(CreateTemplate), Items = items, IsVirtualized = false + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 9f90037032..a06211c040 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -202,41 +202,6 @@ namespace Avalonia.Controls.UnitTests } - } - private ComboBox CreateControl() - { - var control = new ComboBox() - { - Template = GetTemplate() - }; - - control.ApplyTemplate(); - return control; - } - - private TextBox GetTextBox(ComboBox control) - { - return control.GetTemplateChildren() - // .OfType() - // .Select(b => b.Content) - .OfType() - .First(); - } - // private IControlTemplate CreateTemplate() - // { - // return new FuncControlTemplate((control, scope) => - // { - // var textBox = - // new TextBox - // { - // Name = "PART_TextBox" - // }.RegisterInNameScope(scope); - // return new ButtonSpinner - // { - // Name = "PART_Spinner", - // Content = textBox, - // }.RegisterInNameScope(scope); - // }); - // } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d13f5b704d..94ada5520e 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,11 +1,14 @@ using System.Linq; +using System.Reactive.Subjects; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -564,7 +567,23 @@ namespace Avalonia.Controls.UnitTests [Fact] public void SelectedItem_Validation() { + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = new[] { "Foo" }, + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + }; + + Prepare(target); + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Dispatcher.UIThread.RunJobs(); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } } } From d1f44dcdf4d89abf20bba753494a3ac23728fd5e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Mar 2021 11:57:59 +0100 Subject: [PATCH 07/35] Added DevToolsOptions. With options to: - Set the key gesture - Show as child window - Set the initial size Co-Authored-By: workgroupengineering --- .../DevToolsExtensions.cs | 12 ++++++++- .../Diagnostics/DevTools.cs | 24 ++++++++++++----- .../Diagnostics/DevToolsOptions.cs | 26 +++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs index 4bc2ca313f..a432c94a5d 100644 --- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -15,7 +15,7 @@ namespace Avalonia /// The window to attach DevTools to. public static void AttachDevTools(this TopLevel root) { - DevTools.Attach(root, new KeyGesture(Key.F12)); + DevTools.Attach(root, new DevToolsOptions()); } /// @@ -27,5 +27,15 @@ namespace Avalonia { DevTools.Attach(root, gesture); } + + /// + /// Attaches DevTools to a window, to be opened with the specified options. + /// + /// The window to attach DevTools to. + /// additional settint of DevTools + public static void AttachDevTools(this TopLevel root, DevToolsOptions options) + { + DevTools.Attach(root, options); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 4899be2955..7942d22962 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -6,6 +6,8 @@ using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Interactivity; +#nullable enable + namespace Avalonia.Diagnostics { public static class DevTools @@ -13,12 +15,20 @@ namespace Avalonia.Diagnostics private static readonly Dictionary s_open = new Dictionary(); public static IDisposable Attach(TopLevel root, KeyGesture gesture) + { + return Attach(root, new DevToolsOptions() + { + Gesture = gesture, + }); + } + + public static IDisposable Attach(TopLevel root, DevToolsOptions options) { void PreviewKeyDown(object sender, KeyEventArgs e) { - if (gesture.Matches(e)) + if (options.Gesture.Matches(e)) { - Open(root); + Open(root, options); } } @@ -28,7 +38,9 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - public static IDisposable Open(TopLevel root) + public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); + + public static IDisposable Open(TopLevel root, DevToolsOptions options) { if (s_open.TryGetValue(root, out var window)) { @@ -38,15 +50,15 @@ namespace Avalonia.Diagnostics { window = new MainWindow { - Width = 1024, - Height = 512, Root = root, + Width = options.Size.Width, + Height = options.Size.Height, }; window.Closed += DevToolsClosed; s_open.Add(root, window); - if (root is Window inspectedWindow) + if (options.ShowAsChildWindow && root is Window inspectedWindow) { window.Show(inspectedWindow); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs new file mode 100644 index 0000000000..ee46192207 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -0,0 +1,26 @@ +using Avalonia.Input; + +namespace Avalonia.Diagnostics +{ + /// + /// Describes options used to customize DevTools. + /// + public class DevToolsOptions + { + /// + /// Gets or sets the key gesture used to open DevTools. + /// + public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12); + + /// + /// Gets or sets a value indicating whether DevTools should be displayed as a child window + /// of the window being inspected. The default value is true. + /// + public bool ShowAsChildWindow { get; set; } = true; + + /// + /// Gets or sets the initial size of the DevTools window. The default value is 1024x512. + /// + public Size Size { get; set; } = new Size(1024, 512); + } +} From 0a41ba08fd318269763385016339f69e65ba19f4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 12:55:58 +0100 Subject: [PATCH 08/35] Added failing batch update test. --- .../AvaloniaObjectTests_BatchUpdate.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 9f0e52c8d9..050fefbd53 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -449,6 +449,29 @@ namespace Avalonia.Base.UnitTests Assert.Null(raised[1].NewValue); } + [Fact] + public void Can_Set_Cleared_Value_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + + target.Foo = "foo"; + + target.BeginBatchUpdate(); + target.ClearValue(TestClass.FooProperty); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.FooProperty && e.NewValue is null) + { + target.Foo = "bar"; + ++raised; + } + }; + target.EndBatchUpdate(); + + Assert.Equal(1, raised); + } + public class TestClass : AvaloniaObject { public static readonly StyledProperty FooProperty = From f1298fb1bd5d8e91161e0e8dc5d465749470c40b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:28:35 +0100 Subject: [PATCH 09/35] Don't write new values to remove sentinels. During a batch update sentinel values are written to the value store to indicate that the value needs to be removed at the end of the update. If a new value is written to the store in the course of ending the batch update, don't update this sentinel value as the value will subsequently be lost. --- src/Avalonia.Base/ValueStore.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e32b20cc96..470be35592 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -103,7 +103,7 @@ namespace Avalonia IDisposable? result = null; - if (_values.TryGetValue(property, out var slot)) + if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) { result = SetExisting(slot, property, value, priority); } @@ -364,6 +364,14 @@ namespace Avalonia } } + private static bool IsRemoveSentinel(IValue value) + { + // Local value entries are optimized and contain only a single value field to save space, + // so there's no way to mark them for removal at the end of a batch update. Instead a + // ConstantValueEntry with a priority of Unset is used as a sentinel value. + return value is ConstantValueEntry t && t.Priority == BindingPriority.Unset; + } + private class BatchUpdate { private ValueStore _owner; From 03cf2c6f9f79294c3c07a82fa9a59b7e772546f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:30:37 +0100 Subject: [PATCH 10/35] Added another failing batch update test. And a bit of a sanity check to the previous one. --- .../AvaloniaObjectTests_BatchUpdate.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 050fefbd53..5bf3afc9e7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -469,6 +469,31 @@ namespace Avalonia.Base.UnitTests }; target.EndBatchUpdate(); + Assert.Equal("bar", target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void Can_Bind_Cleared_Value_When_Ending_Batch_Update() + { + var target = new TestClass(); + var raised = 0; + + target.Foo = "foo"; + + target.BeginBatchUpdate(); + target.ClearValue(TestClass.FooProperty); + target.PropertyChanged += (sender, e) => + { + if (e.Property == TestClass.FooProperty && e.NewValue is null) + { + target.Bind(TestClass.FooProperty, new TestObservable("bar")); + ++raised; + } + }; + target.EndBatchUpdate(); + + Assert.Equal("bar", target.Foo); Assert.Equal(1, raised); } From 3afa95253f0c517e85af08bcc6d1f3ea074e9ff3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Mar 2021 13:38:29 +0100 Subject: [PATCH 11/35] Allow binding property during ending batch update. - Allow adding a binding to a cleared property while ending a batch update. Need to check that the existing value isn't a remove sentinel here, otherwise the binding will be lost. - When a binding is added during the end of a batch update, `_batchUpdate` will be non-null but newly added bindings shouldn't have `BeginBatchUpdate` called on them because no `EndBatchUpdate` will arrive (as we've already called them) - Add sanity checks to the unit test to make sure that correct notifications are raised --- src/Avalonia.Base/ValueStore.cs | 6 ++++-- .../AvaloniaObjectTests_BatchUpdate.cs | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 470be35592..9ece2b8042 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -138,7 +138,7 @@ namespace Avalonia IObservable> source, BindingPriority priority) { - if (_values.TryGetValue(property, out var slot)) + if (_values.TryGetValue(property, out var slot) && !IsRemoveSentinel(slot)) { return BindExisting(slot, property, source, priority); } @@ -338,7 +338,7 @@ namespace Avalonia private void AddValue(AvaloniaProperty property, IValue value) { _values.AddValue(property, value); - if (_batchUpdate is object && value is IBatchUpdate batch) + if (_batchUpdate?.IsBatchUpdating == true && value is IBatchUpdate batch) batch.BeginBatchUpdate(); value.Start(); } @@ -381,6 +381,8 @@ namespace Avalonia public BatchUpdate(ValueStore owner) => _owner = owner; + public bool IsBatchUpdating => _batchUpdateCount > 0; + public void Begin() { if (_batchUpdateCount++ == 0) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 5bf3afc9e7..036f275a71 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs @@ -478,6 +478,7 @@ namespace Avalonia.Base.UnitTests { var target = new TestClass(); var raised = 0; + var notifications = new List(); target.Foo = "foo"; @@ -490,11 +491,16 @@ namespace Avalonia.Base.UnitTests target.Bind(TestClass.FooProperty, new TestObservable("bar")); ++raised; } + + notifications.Add(e); }; target.EndBatchUpdate(); Assert.Equal("bar", target.Foo); Assert.Equal(1, raised); + Assert.Equal(2, notifications.Count); + Assert.Equal(null, notifications[0].NewValue); + Assert.Equal("bar", notifications[1].NewValue); } public class TestClass : AvaloniaObject From 1c61a5c1c582d2970ba22c4a22a8199b6e662089 Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 09:53:42 +0100 Subject: [PATCH 12/35] fixed failed test --- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 94ada5520e..f3d71962b9 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -572,6 +572,8 @@ namespace Avalonia.Controls.UnitTests Template = ListBoxTemplate(), Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + SelectionMode = SelectionMode.AlwaysSelected, + VirtualizationMode = ItemVirtualizationMode.None, }; Prepare(target); From 2dd6b574d41f5bcae5a2f878ef0fe0def902dc3d Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 10:27:11 +0100 Subject: [PATCH 13/35] fixing failing test --- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index a06211c040..5395dfeadb 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls.UnitTests { var target = new ComboBox { - Template = GetTemplate() + Template = GetTemplate(), + VirtualizationMode = ItemVirtualizationMode.None }; target.ApplyTemplate(); From 2a185da6391bd550d9babc8d1e005135e18dd071 Mon Sep 17 00:00:00 2001 From: aljosas Date: Tue, 23 Mar 2021 14:45:42 +0100 Subject: [PATCH 14/35] removing Dispatcher.UIThread.RunJobs() from tests --- tests/Avalonia.Controls.UnitTests/CarouselTests.cs | 2 -- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 2 -- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 4 +--- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index ea39093602..b93e48618d 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -335,8 +335,6 @@ namespace Avalonia.Controls.UnitTests BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - Dispatcher.UIThread.RunJobs(); - Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 5395dfeadb..8f9c7fdb0b 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -195,8 +195,6 @@ namespace Avalonia.Controls.UnitTests var exception = new System.InvalidCastException("failed validation"); var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - - Dispatcher.UIThread.RunJobs(); Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index f3d71962b9..963bba7c83 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -573,7 +573,7 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None, + VirtualizationMode = ItemVirtualizationMode.None }; Prepare(target); @@ -582,8 +582,6 @@ namespace Avalonia.Controls.UnitTests var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); target.Bind(ComboBox.SelectedItemProperty, textObservable); - Dispatcher.UIThread.RunJobs(); - Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } From 61792ce45faa422f328c6a545304a9b1c93d4982 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Mar 2021 18:30:22 +0300 Subject: [PATCH 15/35] Allow binding to classes with Classes.ClassName --- src/Avalonia.Styling/ClassBindingManager.cs | 38 ++++++++ src/Avalonia.Styling/Controls/Classes.cs | 21 ++++ .../StyledElementExtensions.cs | 11 +++ .../AvaloniaXamlIlCompiler.cs | 5 +- .../AvaloniaXamlIlClassesPropertyResolver.cs | 97 +++++++++++++++++++ ...mlIlReorderClassesPropertiesTransformer.cs | 40 ++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 10 ++ .../Xaml/BindingTests.cs | 32 ++++++ 8 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Styling/ClassBindingManager.cs create mode 100644 src/Avalonia.Styling/StyledElementExtensions.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Styling/ClassBindingManager.cs new file mode 100644 index 0000000000..6ddea934bb --- /dev/null +++ b/src/Avalonia.Styling/ClassBindingManager.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia +{ + internal static class ClassBindingManager + { + private static readonly Dictionary s_RegisteredProperties = + new Dictionary(); + + public static IDisposable Bind(IStyledElement target, string className, IBinding source, object anchor) + { + if (!s_RegisteredProperties.TryGetValue(className, out var prop)) + s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className); + return target.Bind(prop, source, anchor); + } + + private static AvaloniaProperty RegisterClassProxyProperty(string className) + { + var prop = AvaloniaProperty.Register("__AvaloniaReserved::Classes::" + className); + prop.Changed.Subscribe(args => + { + var enable = args.NewValue.GetValueOrDefault(); + var classes = ((IStyledElement)args.Sender).Classes; + if (enable) + { + if (!classes.Contains(className)) + classes.Add(className); + } + else + classes.Remove(className); + }); + + return prop; + } + } +} diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs index 51dca57928..4e2783d4ec 100644 --- a/src/Avalonia.Styling/Controls/Classes.cs +++ b/src/Avalonia.Styling/Controls/Classes.cs @@ -265,5 +265,26 @@ namespace Avalonia.Controls $"The pseudoclass '{name}' may only be {operation} by the control itself."); } } + + /// + /// Adds a or removes a style class to/from the collection. + /// + /// The class names. + /// If true adds the class, if false, removes it. + /// + /// Only standard classes may be added or removed via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public void Set(string name, bool value) + { + if (value) + { + if (!Contains(name)) + Add(name); + } + else + Remove(name); + } } } diff --git a/src/Avalonia.Styling/StyledElementExtensions.cs b/src/Avalonia.Styling/StyledElementExtensions.cs new file mode 100644 index 0000000000..0c5a5f7438 --- /dev/null +++ b/src/Avalonia.Styling/StyledElementExtensions.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Data; + +namespace Avalonia +{ + public static class StyledElementExtensions + { + public static IDisposable BindClass(this IStyledElement target, string className, IBinding source, object anchor) => + ClassBindingManager.Bind(target, className, source, anchor); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index abff763bb1..a191dc59fb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Targeted InsertBefore( + new AvaloniaXamlIlResolveClassesPropertiesTransformer(), new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); InsertAfter( - new AvaloniaXamlIlAvaloniaPropertyResolver()); + new AvaloniaXamlIlAvaloniaPropertyResolver(), + new AvaloniaXamlIlReorderClassesPropertiesTransformer() + ); InsertBefore( new AvaloniaXamlIlBindingPathParser(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs new file mode 100644 index 0000000000..23232dbcf3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveClassesPropertiesTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && prop.DeclaringType is XamlAstClrTypeReference declaringRef) + { + var types = context.GetAvaloniaTypes(); + if (types.StyledElement.IsAssignableFrom(targetRef.Type) + && types.Classes.Equals(declaringRef.Type)) + { + return new XamlAstClrProperty(node, "class:" + prop.Name, types.Classes, + null) + { + Setters = { new ClassValueSetter(types, prop.Name), new ClassBindingSetter(types, prop.Name) } + }; + } + } + return node; + } + + + class ClassValueSetter : IXamlEmitablePropertySetter + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + private readonly string _className; + + public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className) + { + _types = types; + _className = className; + Parameters = new[] { types.XamlIlTypes.Boolean }; + } + + public void Emit(IXamlILEmitter emitter) + { + using (var value = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.Boolean)) + { + emitter + .Stloc(value.Local) + .EmitCall(_types.StyledElementClassesProperty.Getter) + .Ldstr(_className) + .Ldloc(value.Local) + .EmitCall(_types.Classes.GetMethod(new FindMethodMethodSignature("Set", + _types.XamlIlTypes.Void, _types.XamlIlTypes.String, _types.XamlIlTypes.Boolean))); + } + } + + public IXamlType TargetType => _types.StyledElement; + + public PropertySetterBinderParameters BinderParameters { get; } = + new PropertySetterBinderParameters { AllowXNull = false }; + public IReadOnlyList Parameters { get; } + } + + class ClassBindingSetter : IXamlEmitablePropertySetter + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + private readonly string _className; + + public ClassBindingSetter(AvaloniaXamlIlWellKnownTypes types, string className) + { + _types = types; + _className = className; + Parameters = new[] {types.IBinding}; + } + + public void Emit(IXamlILEmitter emitter) + { + using (var bloc = emitter.LocalsPool.GetLocal(_types.IBinding)) + emitter + .Stloc(bloc.Local) + .Ldstr(_className) + .Ldloc(bloc.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(_types.ClassesBindMethod, true); + } + + public IXamlType TargetType => _types.StyledElement; + + public PropertySetterBinderParameters BinderParameters { get; } = + new PropertySetterBinderParameters { AllowXNull = false }; + public IReadOnlyList Parameters { get; } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs new file mode 100644 index 0000000000..ae3515a6d6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs @@ -0,0 +1,40 @@ +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlReorderClassesPropertiesTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode obj) + { + IXamlAstNode classesNode = null; + IXamlAstNode firstSingleClassNode = null; + var types = context.GetAvaloniaTypes(); + foreach (var child in obj.Children) + { + if (child is XamlAstXamlPropertyValueNode propValue + && propValue.Property is XamlAstClrProperty prop) + { + if (prop.DeclaringType.Equals(types.Classes)) + { + if (firstSingleClassNode == null) + firstSingleClassNode = child; + } + else if (prop.Name == "Classes" && prop.DeclaringType.Equals(types.StyledElement)) + classesNode = child; + } + } + + if (classesNode != null && firstSingleClassNode != null) + { + obj.Children.Remove(classesNode); + obj.Children.Insert(obj.Children.IndexOf(firstSingleClassNode), classesNode); + } + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 34aae2c5ed..c4995b2de3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType AssignBindingAttribute { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } + public IXamlType IStyledElement { get; } public IXamlType NameScope { get; } public IXamlMethod NameScopeSetNameScope { get; } public IXamlType INameScope { get; } @@ -78,6 +79,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinition { get; } public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } + public IXamlMethod ClassesBindMethod { get; } + public IXamlProperty StyledElementClassesProperty { get; set; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -97,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IBinding, cfg.WellKnownTypes.Object); UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement"); + IStyledElement = cfg.TypeSystem.GetType("Avalonia.IStyledElement"); INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope"); INameScopeRegister = INameScope.GetMethod( new FindMethodMethodSignature("Register", XamlIlTypes.Void, @@ -168,6 +172,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition"); RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions"); Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes"); + StyledElementClassesProperty = + StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes)); + ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + .FindMethod( "BindClass", IDisposable, false, IStyledElement, + cfg.WellKnownTypes.String, + IBinding, cfg.WellKnownTypes.Object); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 4f2b580bce..8af638c5d7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -377,5 +377,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml public string Greeting1 { get; set; } = "Hello"; public string Greeting2 { get; set; } = "World"; } + + [Fact] + public void Binding_Classes_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + // Note, this test also checks `Classes` reordering, so it should be kept AFTER the last single class + var xaml = @" + + /// The window to attach DevTools to. - /// additional settint of DevTools + /// Additional settings of DevTools. public static void AttachDevTools(this TopLevel root, DevToolsOptions options) { DevTools.Attach(root, options); From 31d7df39c0968a8fc991b6544e7db80a1c48ec3d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 29 Mar 2021 20:35:54 +0100 Subject: [PATCH 27/35] fix datavalidation errors on fluent theme. --- src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index ed31f7b573..f83af266c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -35,7 +35,7 @@ - + From 37e8890d2729d5e14912b3cc1a680d4672194c25 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Tue, 30 Mar 2021 12:40:03 +0200 Subject: [PATCH 28/35] CanExecute reevaluation on attaching to logical tree --- src/Avalonia.Controls/Button.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cb..6093cbd581 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -218,6 +218,7 @@ namespace Avalonia.Controls if (Command != null) { Command.CanExecuteChanged += CanExecuteChanged; + CanExecuteChanged(this, EventArgs.Empty); } } From 741f3458d4ec1e5bb32916e12f3d538b46c483a1 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Thu, 1 Apr 2021 17:44:00 +0200 Subject: [PATCH 29/35] Allowed empty templates --- .../Templates/DataTemplate.cs | 2 +- .../Templates/ItemsPanelTemplate.cs | 3 +-- .../Templates/Template.cs | 2 +- .../Templates/TemplateContent.cs | 8 ++++-- .../Templates/TreeDataTemplate.cs | 8 ++++-- .../Xaml/DataTemplateTests.cs | 25 +++++++++++++++++++ 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 07c5451135..650534b347 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data, IControl existing) { - return existing ?? TemplateContent.Load(Content).Control; + return existing ?? TemplateContent.Load(Content)?.Control; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c8843a3176..c096ed7ed7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IPanel Build() - => (IPanel)TemplateContent.Load(Content).Control; + public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 65323ae665..45fae9cb28 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IControl Build() => TemplateContent.Load(Content).Control; + public IControl Build() => TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 96f25668fb..483a1a5d06 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Controls; -using System.Collections.Generic; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates @@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates { return (ControlTemplateResult)direct(null); } + + if (templateContent is null) + { + return null; + } + throw new ArgumentException(nameof(templateContent)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index d785ac4ac0..7b065c7f47 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data) { - var visualTreeForItem = TemplateContent.Load(Content).Control; - visualTreeForItem.DataContext = data; + var visualTreeForItem = TemplateContent.Load(Content)?.Control; + if (visualTreeForItem != null) + { + visualTreeForItem.DataContext = data; + } + return visualTreeForItem; } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 033b670bf4..53881467e7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { public class DataTemplateTests : XamlTestBase { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Null(target.Presenter.Child); + } + } + [Fact] public void DataTemplate_Can_Contain_Name() { From 65792cf4b037ec3f48d8e8c54de775608a519db8 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 02:21:08 +0300 Subject: [PATCH 30/35] feature: Disable ReactiveUI platform registrations --- build/ReactiveUI.props | 2 +- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index f74ab07e31..c3b136d41d 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 6771d3e179..fa666e2125 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -17,6 +17,8 @@ namespace Avalonia.ReactiveUI { return builder.AfterPlatformServicesSetup(_ => { + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); + Locator.CurrentMutable.InitializeReactiveUI(); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); From cc5568781cf046d8b81efe0fc56137b1fe920499 Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 02:31:08 +0300 Subject: [PATCH 31/35] Don't initialize things twice --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index fa666e2125..e5250484e2 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -18,7 +18,6 @@ namespace Avalonia.ReactiveUI return builder.AfterPlatformServicesSetup(_ => { PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); - Locator.CurrentMutable.InitializeReactiveUI(); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); From 2045097cc800b27df5c881c9970f617634f795ac Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Apr 2021 11:45:58 +0300 Subject: [PATCH 32/35] Wrap UseReactiveUI in RegisterResolverCallbackChanged --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index e5250484e2..359da3d7c2 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -9,19 +9,22 @@ namespace Avalonia.ReactiveUI { /// /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia - /// scheduler and Avalonia activation for view fetcher. Always remember to - /// call this method if you are using ReactiveUI in your application. + /// scheduler, an activation for view fetcher, a template binding hook. Remember + /// to call this method if you are using ReactiveUI in your application. /// public static TAppBuilder UseReactiveUI(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() - { - return builder.AfterPlatformServicesSetup(_ => + where TAppBuilder : AppBuilderBase, new() => + builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => { + if (Locator.CurrentMutable is null) + { + return; + } + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - }); - } + })); } } From 38703b94232a562cfc20a7b50ca971e37a28bc43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Apr 2021 15:29:46 +0200 Subject: [PATCH 33/35] Fix pointer interaction with reversed direction slider. Previously dragging a slider with `IsDirectionRevered = true` resulted in the slider moving the wrong way. --- src/Avalonia.Controls/Slider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e02efc2bd2..6419981fb1 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -341,7 +341,9 @@ namespace Avalonia.Controls var pointNum = orient ? x.Position.X : x.Position.Y; var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); - var invert = orient ? 0 : 1; + var invert = orient ? + IsDirectionReversed ? 1 : 0 : + IsDirectionReversed ? 0 : 1; var calcVal = Math.Abs(invert - logicalPos); var range = Maximum - Minimum; var finalValue = calcVal * range + Minimum; From 2fcad40bce09115ab022a600e26de8a1de4f1903 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 3 Apr 2021 16:27:00 +0800 Subject: [PATCH 34/35] Add Deterministic XamlX ID Generator (#5684) * Add Deterministic XamlX ID Generator * Apply suggestions from code review * simplify stuff and apply review * Update src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs * Update src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs * add the det id gen to runtine xamlx compiler * rerun tests * rerun tests * try this * use id gen instead of guid * a * Update src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs * Update AvaloniaXamlIlCompilerConfiguration.cs revert * Update XamlIlClrPropertyInfoHelper.cs * Update AvaloniaXamlIlRuntimeCompiler.cs * fix * revert hack * make id gen optional --- src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs | 12 ++++++++++++ src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs | 4 ++-- .../AvaloniaXamlIlCompilerConfiguration.cs | 5 +++-- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs new file mode 100644 index 0000000000..f207b558a3 --- /dev/null +++ b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs @@ -0,0 +1,12 @@ +using System; +using XamlX.Transform; + +namespace Avalonia.Build.Tasks +{ + public class DeterministicIdGenerator : IXamlIdentifierGenerator + { + private int _nextId = 1; + + public string GenerateIdentifierPart() => (_nextId++).ToString(); + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 6ef8a98fae..508045dccb 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -22,7 +22,6 @@ using XamlX.IL; namespace Avalonia.Build.Tasks { - public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") @@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), - new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure))); + new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)), + new DeterministicIdGenerator()); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs index 0c0dcb1634..f6f47dce0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlXmlnsMappings xmlnsMappings, XamlValueConverter customValueConverter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter, - XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) - : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter, + IXamlIdentifierGenerator identifierGenerator = null) + : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator) { ClrPropertyEmitter = clrPropertyEmitter; AccessorFactoryEmitter = accessorFactoryEmitter; From 64b939bee84ae923d2d3baf3a01f8ab825a15a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neves?= Date: Mon, 5 Apr 2021 12:41:51 +0100 Subject: [PATCH 35/35] Show and hide native control if visibility of any of its ancestors changes --- src/Avalonia.Controls/NativeControlHost.cs | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 64414b1f47..c2fe1bb691 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -16,30 +16,16 @@ namespace Avalonia.Controls private bool _queuedForDestruction; private bool _queuedForMoveResize; private readonly List _propertyChangedSubscriptions = new List(); - private readonly EventHandler _propertyChangedHandler; - static NativeControlHost() - { - IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); - } - - public NativeControlHost() - { - _propertyChangedHandler = PropertyChangedHandler; - } - - private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) - => host.UpdateHost(); protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; var visual = (IVisual)this; - while (visual != _currentRoot) + while (visual != null) { - if (visual is Visual v) { - v.PropertyChanged += _propertyChangedHandler; + v.PropertyChanged += PropertyChangedHandler; _propertyChangedSubscriptions.Add(v); } @@ -51,7 +37,7 @@ namespace Avalonia.Controls private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) { - if (e.IsEffectiveValueChange && e.Property == BoundsProperty) + if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty)) EnqueueForMoveResize(); } @@ -61,7 +47,7 @@ namespace Avalonia.Controls if (_propertyChangedSubscriptions != null) { foreach (var v in _propertyChangedSubscriptions) - v.PropertyChanged -= _propertyChangedHandler; + v.PropertyChanged -= PropertyChangedHandler; _propertyChangedSubscriptions.Clear(); } UpdateHost();