From e870f6c6e492f7df704d491c0a7b310d2bdffefd Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:00:39 +0100 Subject: [PATCH 01/51] Update NUKE to 0.24 --- nukebuild/Build.cs | 54 ++++++++++++++++++++++------------------- nukebuild/Shims.cs | 8 +++--- nukebuild/_build.csproj | 8 +++--- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7b3b8465ce..b14b78065b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -26,7 +26,7 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; running and debugging a particular target (optionally without deps) would be way easier ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support - + */ partial class Build : NukeBuild @@ -54,7 +54,7 @@ partial class Build : NukeBuild protected override void OnBuildInitialized() { Parameters = new BuildParameters(this); - Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, Parameters.Configuration, typeof(NukeBuild).Assembly.GetName().Version.ToString()); @@ -93,8 +93,10 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(projectFile, c => + return MSBuild(c => { + c = c.SetProjectFile(projectFile); + // This is required for VS2019 image on Azure Pipelines if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) { @@ -114,8 +116,8 @@ partial class Build : NukeBuild } Target Clean => _ => _.Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); + Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(DeleteDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); @@ -134,12 +136,13 @@ partial class Build : NukeBuild ); else - DotNetBuild(Parameters.MSBuildSolution, c => c + DotNetBuild(c => c + .SetProjectFile(Parameters.MSBuildSolution) .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); }); - + void RunCoreTest(string project) { if(!project.EndsWith(".csproj")) @@ -153,13 +156,13 @@ partial class Build : NukeBuild var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); if (targets != null) frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else + else frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - + foreach(var fw in frameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); @@ -184,7 +187,7 @@ partial class Build : NukeBuild } Target RunCoreLibsTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -204,7 +207,7 @@ partial class Build : NukeBuild }); Target RunRenderTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -212,9 +215,9 @@ partial class Build : NukeBuild if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); }); - + Target RunDesignerTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -224,7 +227,7 @@ partial class Build : NukeBuild [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; Target RunLeakTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -235,7 +238,7 @@ partial class Build : NukeBuild }); Target ZipFiles => _ => _ - .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .Executes(() => { var data = Parameters; @@ -259,9 +262,10 @@ partial class Build : NukeBuild MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else - DotNetPack(Parameters.MSBuildSolution, c => - c.SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => c + .SetProject(Parameters.MSBuildSolution) + .SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); }); Target CreateNugetPackages => _ => _ @@ -274,29 +278,29 @@ partial class Build : NukeBuild new NumergeNukeLogger())) throw new Exception("Package merge failed"); }); - + Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunDesignerTests) .DependsOn(RunLeakTests); - + Target Package => _ => _ .DependsOn(RunTests) .DependsOn(CreateNugetPackages); - + Target CiAzureLinux => _ => _ .DependsOn(RunTests); - + Target CiAzureOSX => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + Target CiAzureWindows => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Execute(x => x.Package) diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 461d617643..1ac14bf622 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -19,9 +19,9 @@ public partial class Build Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; @@ -38,7 +38,7 @@ public partial class Build fileStream.CopyTo(entryStream); atLeastOneFileAdded = true; } - + foreach (var path in paths) { if (Directory.Exists(path)) @@ -64,7 +64,7 @@ public partial class Build finished = true; } - finally + finally { try { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a736e4653..f26bf7137e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.0 false False @@ -10,7 +10,7 @@ - + @@ -20,11 +20,11 @@ - + - + From 367548c1bece9d3bfc4b9d54bddaeb0fd55e6e12 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:21:55 +0100 Subject: [PATCH 02/51] Minor cleanups --- nukebuild/Build.cs | 118 +++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index b14b78065b..1a924733b2 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -13,6 +13,7 @@ using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -31,6 +32,8 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; partial class Build : NukeBuild { + [Solution("Avalonia.sln")] readonly Solution Solution; + static Lazy MsBuildExe = new Lazy(() => { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -93,27 +96,20 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(c => - { - c = c.SetProjectFile(projectFile); - + return MSBuild(c => c + .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) - { - var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); - if (javaSdk != null) - c = c.AddProperty("JavaSdkDirectory", javaSdk); - } - - c = c.AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal); - c = configurator?.Invoke(c) ?? c; - return c; - }); + .When(Parameters.IsRunningOnWindows && + Parameters.IsRunningOnAzure, c => c + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", true) + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .Apply(configurator)); } + Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); @@ -143,23 +139,12 @@ partial class Build : NukeBuild ); }); - void RunCoreTest(string project) + void RunCoreTest(string projectName) { - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - XDocument xdoc; - using (var s = File.OpenRead(project)) - xdoc = XDocument.Load(s); - - List frameworks = null; - var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); - if (targets != null) - frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else - frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - - foreach(var fw in frameworks) + Information($"Running tests from {projectName}"); + var project = Solution.GetProject(projectName).NotNull("project != null"); + + foreach (var fw in project.GetTargetFrameworks()) { if (fw.StartsWith("net4") && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) @@ -170,19 +155,16 @@ partial class Build : NukeBuild } Information("Running for " + fw); - DotNetTest(c => - { - c = c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore(); - // NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient - if (Parameters.PublishTestResults) - c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot); - return c; - }); + + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Parameters.Configuration) + .SetFramework(fw) + .EnableNoBuild() + .EnableNoRestore() + .When(Parameters.PublishTestResults, c => c + .SetLogger("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -191,19 +173,19 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Animation.UnitTests"); - RunCoreTest("./tests/Avalonia.Base.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("./tests/Avalonia.Input.UnitTests"); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); - RunCoreTest("./tests/Avalonia.Layout.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("./tests/Avalonia.Styling.UnitTests"); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); - RunCoreTest("./tests/Avalonia.Skia.UnitTests"); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Animation.UnitTests"); + RunCoreTest("Avalonia.Base.UnitTests"); + RunCoreTest("Avalonia.Controls.UnitTests"); + RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); + RunCoreTest("Avalonia.Input.UnitTests"); + RunCoreTest("Avalonia.Interactivity.UnitTests"); + RunCoreTest("Avalonia.Layout.UnitTests"); + RunCoreTest("Avalonia.Markup.UnitTests"); + RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); + RunCoreTest("Avalonia.Styling.UnitTests"); + RunCoreTest("Avalonia.Visuals.UnitTests"); + RunCoreTest("Avalonia.Skia.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.UnitTests"); }); Target RunRenderTests => _ => _ @@ -211,9 +193,9 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); + RunCoreTest("Avalonia.Skia.RenderTests"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); + RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); Target RunDesignerTests => _ => _ @@ -221,7 +203,7 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; @@ -307,3 +289,11 @@ partial class Build : NukeBuild : Execute(x => x.RunTests); } + +public static class ToolSettingsExtensions +{ + public static T Apply(this T settings, Configure configurator) + { + return configurator != null ? configurator(settings) : settings; + } +} From d60603d52968ad5c89ab7ef6594a57caf002474d Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:55:54 +0100 Subject: [PATCH 03/51] Fix information output --- nukebuild/Build.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 1a924733b2..c2fa54ba2b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -150,11 +150,11 @@ partial class Build : NukeBuild && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); continue; } - Information("Running for " + fw); + Information($"Running for {projectName} ({fw}) ..."); DotNetTest(c => c .SetProjectFile(project) From 8f68d2d12407dfe1b41bfaa7fdd728f76f8ccc6c Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:56:34 +0100 Subject: [PATCH 04/51] Fix ensuring directory --- nukebuild/Build.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index c2fa54ba2b..358846a14e 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -113,7 +113,7 @@ partial class Build : NukeBuild Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); - Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(EnsureCleanDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); From 924064be139c9a2847d066aba19e3d5896219b42 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 12:31:39 +0100 Subject: [PATCH 05/51] Use netcoreapp3.1 --- nukebuild/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index f26bf7137e..584c36d033 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 false False From 2bf2e60ae04604ded6bd0dbce0807ca1b8762711 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:00:45 +0100 Subject: [PATCH 06/51] Don't add extra pixel to AccessText measurement. It's not needed; the underscore can be drawn in the descender space. --- src/Avalonia.Controls/Primitives/AccessText.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 5adc8d2448..f6fea89ec9 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -84,17 +84,6 @@ namespace Avalonia.Controls.Primitives return base.CreateFormattedText(constraint, StripAccessKey(text)); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - protected override Size MeasureOverride(Size availableSize) - { - var result = base.MeasureOverride(availableSize); - return result.WithHeight(result.Height + 1); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { From 8a0ccea2731e2df48d105f9e5fea25e6f1361fb9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:19 +0100 Subject: [PATCH 07/51] Add MenuItem.InputGestureText. --- src/Avalonia.Controls/MenuItem.cs | 51 +++++++++++++++++++++++ src/Avalonia.Themes.Default/MenuItem.xaml | 26 ++++++++---- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e0baa5e679..ae36b5d830 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -48,6 +49,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InputGestureTextProperty = + AvaloniaProperty.Register(nameof(InputGestureText)); + /// /// Defines the property. /// @@ -93,6 +100,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; + private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -194,6 +202,19 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the input gesture that will be displayed in the menu item. + /// + /// + /// Setting this property does not cause the input gesture to be handled by the menu item, + /// it simply displays the gesture text in the menu. + /// + public object InputGestureText + { + get { return GetValue(InputGestureTextProperty); } + set { SetValue(InputGestureTextProperty, value); } + } + /// /// Gets or sets a value indicating whether the is currently selected. /// @@ -306,6 +327,36 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.GetVisualParent() is IControl parent) + { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + _gridHack = Bind( + DefinitionBase.PrivateSharedSizeScopeProperty, + parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty)); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _gridHack.Dispose(); + _gridHack = null; + } + /// /// Called when the is clicked. /// diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 93989d3782..431adacb47 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -11,7 +11,14 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + + + + + + + + + + Grid.Column="4"/> + Margin="4 2" + Grid.IsSharedSizeScope="True"/> @@ -102,10 +113,11 @@ BorderThickness="{TemplateBinding BorderThickness}"> + Items="{TemplateBinding Items}" + ItemsPanel="{TemplateBinding ItemsPanel}" + ItemTemplate="{TemplateBinding ItemTemplate}" + Margin="2" + Grid.IsSharedSizeScope="True"/> From deebe6090ffddfff6e5da007d9c56a68d2ffe5a5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:33 +0100 Subject: [PATCH 08/51] Show input gesture text in control catalog. --- samples/ControlCatalog/Pages/MenuPage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..cae5ab54b1 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + From 467288ca998a5585955d8cbcf63903c5b3a7c4de Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sat, 22 Feb 2020 18:42:25 +0100 Subject: [PATCH 09/51] Update NUKE to 0.24 --- nukebuild/BuildParameters.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 65ba5e9756..149716b416 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -4,24 +4,21 @@ using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; -using Nuke.Common.Execution; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.MSBuild.MSBuildTasks; public partial class Build { [Parameter("configuration")] public string Configuration { get; set; } - + [Parameter("skip-tests")] public bool SkipTests { get; set; } - + [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } - + public class BuildParameters { public string Configuration { get; } @@ -79,15 +76,15 @@ public partial class Build IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, From b966bd390c7d310590ef98baa0c9aa69440cacce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 23 Feb 2020 15:34:04 +0100 Subject: [PATCH 10/51] Enable NRT in Avalonia.Interactivity. --- .../Avalonia.Interactivity.csproj | 6 +- .../EventSubscription.cs | 20 ++++-- src/Avalonia.Interactivity/IInteractive.cs | 2 +- src/Avalonia.Interactivity/Interactive.cs | 68 ++++++++----------- .../InteractiveExtensions.cs | 5 +- src/Avalonia.Interactivity/RoutedEvent.cs | 18 ++--- src/Avalonia.Interactivity/RoutedEventArgs.cs | 8 +-- .../RoutedEventRegistry.cs | 6 +- 8 files changed, 69 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 66f1e8cc26..730ca2bd6e 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 @@ -9,6 +11,4 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index e8fb1bfaf1..d363e3f6fa 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -9,12 +9,24 @@ namespace Avalonia.Interactivity internal class EventSubscription { - public HandlerInvokeSignature InvokeAdapter { get; set; } + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + HandlerInvokeSignature? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } - public Delegate Handler { get; set; } + public HandlerInvokeSignature? InvokeAdapter { get; } - public RoutingStrategies Routes { get; set; } + public Delegate Handler { get; } - public bool AlsoIfHandled { get; set; } + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } } } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 47046b58e2..6524794733 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -13,7 +13,7 @@ namespace Avalonia.Interactivity /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive InteractiveParent { get; } + IInteractive? InteractiveParent { get; } /// /// Adds a handler for the specified routed event. diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 27ece25183..0c4649a1ca 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -15,16 +15,16 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable, IInteractive { - private Dictionary> _eventHandlers; + private Dictionary>? _eventHandlers; private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; + IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); + private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); /// /// Adds a handler for the specified routed event. @@ -40,16 +40,10 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - var subscription = new EventSubscription - { - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + var subscription = new EventSubscription(handler, routes, handledEventsToo); return AddEventSubscription(routedEvent, subscription); } @@ -68,12 +62,12 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); // EventHandler delegate is not covariant, this forces us to create small wrapper // that will cast our type erased instance and invoke it. - Type eventArgsType = routedEvent.EventArgsType; + var eventArgsType = routedEvent.EventArgsType; if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) { @@ -90,14 +84,7 @@ namespace Avalonia.Interactivity s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); } - var subscription = new EventSubscription - { - InvokeAdapter = invokeAdapter, - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; - + var subscription = new EventSubscription(handler, routes, handledEventsToo, invokeAdapter); return AddEventSubscription(routedEvent, subscription); } @@ -108,12 +95,11 @@ namespace Avalonia.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - List subscriptions = null; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions) == true) { subscriptions.RemoveAll(x => x.Handler == handler); } @@ -137,9 +123,14 @@ namespace Avalonia.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); - e.Source = e.Source ?? this; + if (e.RoutedEvent == null) + { + throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); + } + + e.Source ??= this; if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) { @@ -167,7 +158,7 @@ namespace Avalonia.Interactivity /// The event args. private void BubbleEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Bubble; @@ -182,7 +173,7 @@ namespace Avalonia.Interactivity /// The event args. private void TunnelEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Tunnel; @@ -197,18 +188,17 @@ namespace Avalonia.Interactivity /// The event args. private void RaiseEventImpl(RoutedEventArgs e) { - Contract.Requires(e != null); - - e.RoutedEvent.InvokeRaised(this, e); + e = e ?? throw new ArgumentNullException(nameof(e)); - List subscriptions = null; + e.RoutedEvent!.InvokeRaised(this, e); - if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) { foreach (var sub in subscriptions.ToList()) { bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.AlsoIfHandled; + bool notFinished = !e.Handled || sub.HandledEventsToo; if (correctRoute && notFinished) { @@ -313,7 +303,7 @@ namespace Avalonia.Interactivity { _preTraverse.Execute(target, _args); - IInteractive parent = target.InteractiveParent; + var parent = target.InteractiveParent; if (parent != null) { diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 07e4029240..414c408080 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -30,6 +28,9 @@ namespace Avalonia.Interactivity bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { + o = o ?? throw new ArgumentNullException(nameof(o)); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + return Observable.Create(x => o.AddHandler( routedEvent, (_, e) => x.OnNext(e), diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 55d9e61d87..164a86fab7 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -25,10 +25,14 @@ namespace Avalonia.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)); + name = name ?? throw new ArgumentNullException(nameof(name)); + eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name)); + ownerType = ownerType ?? throw new ArgumentNullException(nameof(name)); + + if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)) + { + throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs."); + } EventArgsType = eventArgsType; Name = name; @@ -52,7 +56,7 @@ namespace Avalonia.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); @@ -65,7 +69,7 @@ namespace Avalonia.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); RoutedEventRegistry.Instance.Register(ownerType, routedEvent); @@ -108,8 +112,6 @@ namespace Avalonia.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } [Obsolete("Use overload taking Action.")] diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Interactivity/RoutedEventArgs.cs index 05bbf7b6a3..e00393322d 100644 --- a/src/Avalonia.Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Interactivity/RoutedEventArgs.cs @@ -11,12 +11,12 @@ namespace Avalonia.Interactivity { } - public RoutedEventArgs(RoutedEvent routedEvent) + public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) + public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; @@ -24,10 +24,10 @@ namespace Avalonia.Interactivity public bool Handled { get; set; } - public RoutedEvent RoutedEvent { get; set; } + public RoutedEvent? RoutedEvent { get; set; } public RoutingStrategies Route { get; set; } - public IInteractive Source { get; set; } + public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs index 34c970a806..0111b115e6 100644 --- a/src/Avalonia.Interactivity/RoutedEventRegistry.cs +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -32,8 +32,8 @@ namespace Avalonia.Interactivity /// public void Register(Type type, RoutedEvent @event) { - Contract.Requires(type != null); - Contract.Requires(@event != null); + type = type ?? throw new ArgumentNullException(nameof(type)); + @event = @event ?? throw new ArgumentNullException(nameof(@event)); if (!_registeredRoutedEvents.TryGetValue(type, out var list)) { @@ -66,7 +66,7 @@ namespace Avalonia.Interactivity /// All routed events registered with the provided type. public IReadOnlyList GetRegistered(Type type) { - Contract.Requires(type != null); + type = type ?? throw new ArgumentNullException(nameof(type)); if (_registeredRoutedEvents.TryGetValue(type, out var events)) { From 4e62ff3ffb9bb68dd13153f357298ac6eeddcf0e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:04:20 +0100 Subject: [PATCH 11/51] Added failing test for #3176. --- .../InteractiveTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 414e67bb94..0355078a05 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -358,6 +358,29 @@ namespace Avalonia.Interactivity.UnitTests Assert.Equal(1, called); } + [Fact] + public void Removing_Control_In_Handler_Should_Not_Stop_Event() + { + // Issue #3176 + var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive)); + var invoked = new List(); + EventHandler handler = (s, e) => invoked.Add(((TestInteractive)s).Name); + var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel); + var target = (IInteractive)parent.GetVisualChildren().Single(); + + EventHandler removeHandler = (s, e) => + { + parent.Children = Array.Empty(); + }; + + target.AddHandler(ev, removeHandler); + + var args = new RoutedEventArgs(ev, target); + target.RaiseEvent(args); + + Assert.Equal(new[] { "3", "2b", "1" }, invoked); + } + private TestInteractive CreateTree( RoutedEvent ev, EventHandler handler, @@ -414,6 +437,7 @@ namespace Avalonia.Interactivity.UnitTests set { + VisualChildren.Clear(); VisualChildren.AddRange(value.Cast()); } } From cca4247c05ce516a904b149ceea972b0ac81dbb4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 10:40:34 +0100 Subject: [PATCH 12/51] Added EventRoute. Instead of traversing the tree while raising an event, instead first build an event route and then raise the event along it. Fixes #3176 --- src/Avalonia.Interactivity/EventRoute.cs | 200 ++++++++++++++++++ .../EventSubscription.cs | 6 +- src/Avalonia.Interactivity/IInteractive.cs | 7 + src/Avalonia.Interactivity/Interactive.cs | 195 +++++------------ src/Avalonia.Interactivity/RoutedEvent.cs | 2 + .../InteractiveTests.cs | 1 - tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 7 files changed, 266 insertions(+), 147 deletions(-) create mode 100644 src/Avalonia.Interactivity/EventRoute.cs diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Interactivity/EventRoute.cs new file mode 100644 index 0000000000..85ba33d7ba --- /dev/null +++ b/src/Avalonia.Interactivity/EventRoute.cs @@ -0,0 +1,200 @@ +using System; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Interactivity +{ + /// + /// Holds the route for a routed event and supports raising an event on that route. + /// + public class EventRoute : IDisposable + { + private readonly RoutedEvent _event; + private PooledList? _route; + + /// + /// Initializes a new instance of the class. + /// + /// The routed event to be raised. + public EventRoute(RoutedEvent e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + _event = e; + _route = null; + } + + /// + /// Gets a value indicating whether the route has any handlers. + /// + public bool HasHandlers => _route?.Count > 0; + + /// + /// Adds a handler to the route. + /// + /// The target on which the event should be raised. + /// The handler for the event. + /// The routing strategies to listen to. + /// + /// If true the handler will be raised even when the routed event is marked as handled. + /// + /// + /// An optional adapter which if supplied, will be called with + /// and the parameters for the event. This adapter can be used to avoid calling + /// `DynamicInvoke` on the handler. + /// + public void Add( + IInteractive target, + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo = false, + Action? adapter = null) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); + } + + /// + /// Adds a class handler to the route. + /// + /// The target on which the event should be raised. + public void AddClassHandler(IInteractive target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, null, null, 0, false)); + } + + /// + /// Raises an event along the route. + /// + /// The event source. + /// The event args. + public void RaiseEvent(IInteractive source, RoutedEventArgs e) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + e = e ?? throw new ArgumentNullException(nameof(e)); + + e.Source = source; + + if (_event.RoutingStrategies == RoutingStrategies.Direct) + { + e.Route = RoutingStrategies.Direct; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + else + { + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) + { + e.Route = RoutingStrategies.Tunnel; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) + { + e.Route = RoutingStrategies.Bubble; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + } + } + + /// + /// Disposes of the event route. + /// + public void Dispose() + { + _route?.Dispose(); + _route = null; + } + + private void RaiseEventImpl(RoutedEventArgs e) + { + if (_route is null) + { + return; + } + + if (e.Source is null) + { + throw new ArgumentException("Event source may not be null", nameof(e)); + } + + IInteractive? lastTarget = null; + var start = 0; + var end = _route.Count; + var step = 1; + + if (e.Route == RoutingStrategies.Tunnel) + { + start = end - 1; + step = end = -1; + } + + for (var i = start; i != end; i += step) + { + var entry = _route[i]; + + // If we've got to a new control then call any RoutedEvent.Raised listeners. + if (entry.Target != lastTarget) + { + if (!e.Handled) + { + _event.InvokeRaised(entry.Target, e); + } + + // If this is a direct event and we've already raised events then we're finished. + if (e.Route == RoutingStrategies.Direct && lastTarget is object) + { + return; + } + + lastTarget = entry.Target; + } + + // Raise the event handler. + if (entry.Handler is object && + entry.Routes.HasFlagCustom(e.Route) && + (!e.Handled || entry.HandledEventsToo)) + { + if (entry.Adapter is object) + { + entry.Adapter(entry.Handler, entry.Target, e); + } + else + { + entry.Handler.DynamicInvoke(entry.Target, e); + } + } + } + } + + private readonly struct RouteItem + { + public RouteItem( + IInteractive target, + Delegate? handler, + Action? adapter, + RoutingStrategies routes, + bool handledEventsToo) + { + Target = target; + Handler = handler; + Adapter = adapter; + Routes = routes; + HandledEventsToo = handledEventsToo; + } + + public IInteractive Target { get; } + public Delegate? Handler { get; } + public Action? Adapter { get; } + public RoutingStrategies Routes { get; } + public bool HandledEventsToo { get; } + } + } +} diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index d363e3f6fa..50f64f49ee 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -5,15 +5,13 @@ using System; namespace Avalonia.Interactivity { - internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); - internal class EventSubscription { public EventSubscription( Delegate handler, RoutingStrategies routes, bool handledEventsToo, - HandlerInvokeSignature? invokeAdapter = null) + Action? invokeAdapter = null) { Handler = handler; Routes = routes; @@ -21,7 +19,7 @@ namespace Avalonia.Interactivity InvokeAdapter = invokeAdapter; } - public HandlerInvokeSignature? InvokeAdapter { get; } + public Action? InvokeAdapter { get; } public Delegate Handler { get; } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 6524794733..33baa9453a 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -60,6 +60,13 @@ namespace Avalonia.Interactivity void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + /// /// Raises a routed event. /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 0c4649a1ca..5a27192c87 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Avalonia.Layout; using Avalonia.VisualTree; @@ -17,15 +15,14 @@ namespace Avalonia.Interactivity { private Dictionary>? _eventHandlers; - private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); + private static readonly Dictionary> s_invokeHandlerCache + = new Dictionary>(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); - /// /// Adds a handler for the specified routed event. /// @@ -130,105 +127,83 @@ namespace Avalonia.Interactivity throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); } - e.Source ??= this; - - if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) - { - e.Route = RoutingStrategies.Direct; - RaiseEventImpl(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) - { - TunnelEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) - { - BubbleEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + using var route = BuildEventRoute(e.RoutedEvent); + route.RaiseEvent(this, e); } - /// - /// Bubbles an event. - /// - /// The event args. - private void BubbleEvent(RoutedEventArgs e) + void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route) { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Bubble; - - var traverser = HierarchyTraverser.Create(e); - - traverser.Traverse(this); - } - - /// - /// Tunnels an event. - /// - /// The event args. - private void TunnelEvent(RoutedEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Tunnel; - - var traverser = HierarchyTraverser.Create(e); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + route = route ?? throw new ArgumentNullException(nameof(route)); - traverser.Traverse(this); + if (_eventHandlers != null && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + foreach (var sub in subscriptions) + { + route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter); + } + } } /// - /// Carries out the actual invocation of an event on this object. + /// Builds an event route for a routed event. /// - /// The event args. - private void RaiseEventImpl(RoutedEventArgs e) + /// The routed event. + /// An describing the route. + /// + /// Usually, calling is sufficent to raise a routed + /// event, however there are situations in which the construction of the event args is expensive + /// and should be avoided if there are no handlers for an event. In these cases you can call + /// this method to build the event route and check the + /// property to see if there are any handlers registered on the route. If there are, call + /// to raise the event. + /// + protected EventRoute BuildEventRoute(RoutedEvent e) { e = e ?? throw new ArgumentNullException(nameof(e)); - e.RoutedEvent!.InvokeRaised(this, e); + var result = new EventRoute(e); + var hasClassHandlers = e.HasRaisedSubscriptions; - if (_eventHandlers is object && - _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) + if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) || + e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) { - foreach (var sub in subscriptions.ToList()) - { - bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.HandledEventsToo; + IInteractive? element = this; - if (correctRoute && notFinished) + while (element != null) + { + if (hasClassHandlers) { - if (sub.InvokeAdapter != null) - { - sub.InvokeAdapter(sub.Handler, this, e); - } - else - { - sub.Handler.DynamicInvoke(this, e); - } + result.AddClassHandler(element); } + + element.AddToEventRoute(e, result); + element = element.InteractiveParent; } } - } - - private List GetEventSubscriptions(RoutedEvent routedEvent) - { - if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + else { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); + if (hasClassHandlers) + { + result.AddClassHandler(this); + } + + ((IInteractive)this).AddToEventRoute(e, result); } - return subscriptions; + return result; } private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { - List subscriptions = GetEventSubscriptions(routedEvent); + _eventHandlers ??= new Dictionary>(); + + if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + subscriptions = new List(); + _eventHandlers.Add(routedEvent, subscriptions); + } subscriptions.Add(subscription); @@ -251,67 +226,5 @@ namespace Avalonia.Interactivity _subscriptions.Remove(_subscription); } } - - private interface ITraverse - { - void Execute(IInteractive target, RoutedEventArgs e); - } - - private struct NopTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - } - } - - private struct RaiseEventTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - ((Interactive)target).RaiseEventImpl(e); - } - } - - /// - /// Traverses interactive hierarchy allowing for raising events. - /// - /// Called before parent is traversed. - /// Called after parent has been traversed. - private struct HierarchyTraverser - where TPreTraverse : struct, ITraverse - where TPostTraverse : struct, ITraverse - { - private TPreTraverse _preTraverse; - private TPostTraverse _postTraverse; - private readonly RoutedEventArgs _args; - - private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args) - { - _preTraverse = preTraverse; - _postTraverse = postTraverse; - _args = args; - } - - public static HierarchyTraverser Create(RoutedEventArgs args) - { - return new HierarchyTraverser(new TPreTraverse(), new TPostTraverse(), args); - } - - public void Traverse(IInteractive target) - { - _preTraverse.Execute(target, _args); - - var parent = target.InteractiveParent; - - if (parent != null) - { - Traverse(parent); - } - - _postTraverse.Execute(target, _args); - } - } } } diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 164a86fab7..e515efd3b4 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -48,6 +48,8 @@ namespace Avalonia.Interactivity public RoutingStrategies RoutingStrategies { get; } + public bool HasRaisedSubscriptions => _raised.HasObservers; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 0355078a05..ef3770d1d9 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; using Xunit; diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index f6454a9cd2..bf75b40a72 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -56,7 +56,7 @@ namespace Avalonia.UnitTests { _pressedButton = mouseButton; _pointer.Capture((IInputElement)target); - target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, + source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, GetModifiers(modifiers), clickCount)); } } From 0f7e3e1b8286c15aa85eaa56cbb8cce1e86b989c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:09:24 +0100 Subject: [PATCH 13/51] Make EventSubscription a private class. --- .../EventSubscription.cs | 30 ------------------- src/Avalonia.Interactivity/Interactive.cs | 23 ++++++++++++++ 2 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 src/Avalonia.Interactivity/EventSubscription.cs diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs deleted file mode 100644 index 50f64f49ee..0000000000 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Interactivity -{ - internal class EventSubscription - { - public EventSubscription( - Delegate handler, - RoutingStrategies routes, - bool handledEventsToo, - Action? invokeAdapter = null) - { - Handler = handler; - Routes = routes; - HandledEventsToo = handledEventsToo; - InvokeAdapter = invokeAdapter; - } - - public Action? InvokeAdapter { get; } - - public Delegate Handler { get; } - - public RoutingStrategies Routes { get; } - - public bool HandledEventsToo { get; } - } -} diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 5a27192c87..6992ebcf34 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -210,6 +210,29 @@ namespace Avalonia.Interactivity return new UnsubscribeDisposable(subscriptions, subscription); } + private sealed class EventSubscription + { + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + Action? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } + + public Action? InvokeAdapter { get; } + + public Delegate Handler { get; } + + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } + } + private sealed class UnsubscribeDisposable : IDisposable { private readonly List _subscriptions; From 2e99fa9a91607abf41e6d82dbcb939917bb1edf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Feb 2020 10:29:16 +0100 Subject: [PATCH 14/51] Make setting styled values disposable. --- src/Avalonia.Base/AvaloniaObject.cs | 9 +++-- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 17 +++++---- src/Avalonia.Base/AvaloniaProperty.cs | 5 ++- src/Avalonia.Base/DirectPropertyBase.cs | 4 ++- src/Avalonia.Base/IAvaloniaObject.cs | 2 +- .../PropertyStore/ConstantValueEntry.cs | 11 ++++-- .../PropertyStore/PriorityValue.cs | 9 +++-- src/Avalonia.Base/StyledPropertyBase.cs | 6 ++-- src/Avalonia.Base/ValueStore.cs | 25 ++++++++----- .../AvaloniaObjectTests_SetValue.cs | 35 +++++++++++++++++++ .../AvaloniaPropertyTests.cs | 2 +- .../PriorityValueTests.cs | 6 +++- 12 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index b0ff591682..88b99cd99a 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -311,7 +311,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) @@ -335,8 +338,10 @@ namespace Avalonia } else if (!(value is DoNothingType)) { - Values.SetValue(property, value, priority); + return Values.SetValue(property, value, priority); } + + return null; } /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index a4c7fa95a5..0f82042dcd 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -458,7 +458,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, object value, @@ -467,7 +470,7 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - property.RouteSetValue(target, value, priority); + return property.RouteSetValue(target, value, priority); } /// @@ -478,7 +481,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, T value, @@ -490,11 +496,10 @@ namespace Avalonia switch (property) { case StyledPropertyBase styled: - target.SetValue(styled, value, priority); - break; + return target.SetValue(styled, value, priority); case DirectPropertyBase direct: target.SetValue(direct, value); - break; + return null; default: throw new NotSupportedException("Unsupported AvaloniaProperty type."); } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index aa7a675764..b0858f8bc7 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,7 +496,10 @@ namespace Avalonia /// The object instance. /// The value. /// The priority. - internal abstract void RouteSetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + internal abstract IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 39ed3b084f..d0dd841a70 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -114,7 +114,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -133,6 +133,8 @@ namespace Avalonia { throw v.Error!; } + + return null; } /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index fb85ae222c..81a212b087 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -65,7 +65,7 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - void SetValue( + IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue); diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index f15f56e32b..aa054c46ff 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry + internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { + private IValueSink _sink; + public ConstantValueEntry( StyledPropertyBase property, T value, - BindingPriority priority) + BindingPriority priority, + IValueSink sink) { Property = property; Value = value; Priority = priority; + _sink = sink; } public StyledPropertyBase Property { get; } @@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore Optional IValue.Value => Value.ToObject(); BindingPriority IValue.ValuePriority => Priority; - public void Reparent(IValueSink sink) { } + public void Dispose() => _sink.Completed(Property, this, Value); + public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 4ef8f650fa..affb20f334 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -78,8 +78,10 @@ namespace Avalonia.PropertyStore public void ClearLocalValue() => UpdateEffectiveValue(); - public void SetValue(T value, BindingPriority priority) + public IDisposable? SetValue(T value, BindingPriority priority) { + IDisposable? result = null; + if (priority == BindingPriority.LocalValue) { _localValue = value; @@ -87,10 +89,13 @@ namespace Avalonia.PropertyStore else { var insert = FindInsertPoint(priority); - _entries.Insert(insert, new ConstantValueEntry(Property, value, priority)); + var entry = new ConstantValueEntry(Property, value, priority, this); + _entries.Insert(insert, entry); + result = entry; } UpdateEffectiveValue(); + return result; } public BindingEntry AddBinding(IObservable> source, BindingPriority priority) diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index d1f961a567..53fcb51c5b 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -194,7 +194,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -203,7 +203,7 @@ namespace Avalonia if (v.HasValue) { - o.SetValue(this, (TValue)v.Value, priority); + return o.SetValue(this, (TValue)v.Value, priority); } else if (v.Type == BindingValueType.UnsetValue) { @@ -213,6 +213,8 @@ namespace Avalonia { throw v.Error; } + + return null; } /// diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e310be0f0a..104c06de0f 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -70,23 +70,25 @@ namespace Avalonia return false; } - public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) { if (property.ValidateValue?.Invoke(value) == false) { throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); } + IDisposable? result = null; + if (_values.TryGetValue(property, out var slot)) { - SetExisting(slot, property, value, priority); + result = SetExisting(slot, property, value, priority); } else if (property.HasCoercion) { // If the property has any coercion callbacks then always create a PriorityValue. var entry = new PriorityValue(_owner, property, this); _values.AddValue(property, entry); - entry.SetValue(value, priority); + result = entry.SetValue(value, priority); } else if (priority == BindingPriority.LocalValue) { @@ -95,10 +97,13 @@ namespace Avalonia } else { - var entry = new ConstantValueEntry(property, value, priority); + var entry = new ConstantValueEntry(property, value, priority, this); _values.AddValue(property, entry); _sink.ValueChanged(property, priority, default, value); + result = entry; } + + return result; } public IDisposable AddBinding( @@ -205,21 +210,23 @@ namespace Avalonia } } - private void SetExisting( + private IDisposable? SetExisting( object slot, StyledPropertyBase property, T value, BindingPriority priority) { + IDisposable? result = null; + if (slot is IPriorityValueEntry e) { var priorityValue = new PriorityValue(_owner, property, this, e); _values.SetValue(property, priorityValue); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); } else if (slot is PriorityValue p) { - p.SetValue(value, priority); + result = p.SetValue(value, priority); } else if (slot is LocalValueEntry l) { @@ -232,7 +239,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } } @@ -240,6 +247,8 @@ namespace Avalonia { throw new NotSupportedException("Unrecognised value store slot type."); } + + return result; } private IDisposable BindExisting( diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 4b477287e8..1b8cd787f2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -284,6 +284,41 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); } + [Fact] + public void Disposing_Style_SetValue_Reverts_To_DefaultValue() + { + Class1 target = new Class1(); + + var d = target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Style_SetValue_Reverts_To_Previous_Style_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Animation_SetValue_Reverts_To_Previous_Local_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.LocalValue); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 6bb8dfe1f5..788376b2fd 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -146,7 +146,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 8c76445645..5e69b8490d 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -24,7 +24,11 @@ namespace Avalonia.Base.UnitTests Owner, TestProperty, NullSink, - new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); + new ConstantValueEntry( + TestProperty, + "1", + BindingPriority.StyleTrigger, + NullSink)); Assert.Equal("1", target.Value.Value); Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); From b82ca9aca987d6ba62269743e61bbd0ab1ad77ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Feb 2020 11:17:06 +0100 Subject: [PATCH 15/51] Add an AvaloniaProperty visitor. We already have some specific internal methods for routing certain methods via an untyped property to a typed property, but adding support for the visitor pattern allows us to support arbitrary use-cases. --- src/Avalonia.Base/AvaloniaProperty.cs | 9 +++++ src/Avalonia.Base/DirectPropertyBase.cs | 7 ++++ src/Avalonia.Base/StyledPropertyBase.cs | 7 ++++ .../Utilities/IAvaloniaPropertyVisitor.cs | 34 +++++++++++++++++++ .../AvaloniaPropertyTests.cs | 6 ++++ 5 files changed, 63 insertions(+) create mode 100644 src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index b0858f8bc7..394e22eac1 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -469,6 +469,15 @@ namespace Avalonia return Name; } + /// + /// Uses the visitor pattern to resolve an untyped property to a typed property. + /// + /// The type of user data passed. + /// The visitor which will accept the typed property. + /// The user data to pass. + public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + where TData : struct; + /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d0dd841a70..d3b5277c53 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Utilities; #nullable enable @@ -101,6 +102,12 @@ namespace Avalonia return (DirectPropertyMetadata)base.GetMetadata(type); } + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + vistor.Visit(this, ref data); + } + /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 53fcb51c5b..fb07ef3f62 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia { @@ -169,6 +170,12 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + vistor.Visit(this, ref data); + } + /// /// Gets the string representation of the property. /// diff --git a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs new file mode 100644 index 0000000000..4b889eb129 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace Avalonia.Utilities +{ + /// + /// A visitor to resolve an untyped to a typed property. + /// + /// The type of user data passed. + /// + /// Pass an instance that implements this interface to + /// + /// in order to resolve un untyped to a typed + /// or . + /// + public interface IAvaloniaPropertyVisitor + where TData : struct + { + /// + /// Called when the property is a styled property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(StyledPropertyBase property, ref TData data); + + /// + /// Called when the property is a direct property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(DirectPropertyBase property, ref TData data); + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 788376b2fd..c7eebdd70a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Data; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests @@ -123,6 +124,11 @@ namespace Avalonia.Base.UnitTests OverrideMetadata(typeof(T), metadata); } + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + throw new NotImplementedException(); + } + internal override IDisposable RouteBind( IAvaloniaObject o, IObservable> source, From dc55d6528775ddd8ccc7b31257c02ba3f0c7fcd8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Feb 2020 17:52:06 +0100 Subject: [PATCH 16/51] Refactored styling. - Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable`-lite in order to cut down on allocations. - #nullable enable on touched files --- .../Diagnostics/ViewModels/TreeNode.cs | 17 +- src/Avalonia.Styling/Avalonia.Styling.csproj | 1 - .../Controls/NameScopeLocator.cs | 6 - src/Avalonia.Styling/StyledElement.cs | 138 +++++++------- .../Styling/ActivatedObservable.cs | 77 -------- .../Styling/ActivatedSubject.cs | 110 ------------ .../Styling/ActivatedValue.cs | 133 -------------- .../Styling/Activators/AndActivator.cs | 67 +++++++ .../Styling/Activators/IStyleActivator.cs | 33 ++++ .../Styling/Activators/IStyleActivatorSink.cs | 17 ++ .../Styling/Activators/NotActivator.cs | 13 ++ .../Styling/Activators/OrActivator.cs | 67 +++++++ .../Activators/PropertyEqualsActivator.cs | 35 ++++ .../Styling/Activators/StyleActivatorBase.cs | 55 ++++++ .../Styling/Activators/StyleClassActivator.cs | 72 ++++++++ .../Styling/DescendentSelector.cs | 48 +++-- src/Avalonia.Styling/Styling/ISetter.cs | 19 +- .../Styling/ISetterInstance.cs | 20 +++ src/Avalonia.Styling/Styling/IStyle.cs | 16 +- .../Styling/IStyleInstance.cs | 22 +++ src/Avalonia.Styling/Styling/IStyleable.cs | 20 ++- src/Avalonia.Styling/Styling/NotSelector.cs | 16 +- src/Avalonia.Styling/Styling/OrSelector.cs | 53 ++++-- .../Styling/PropertyEqualsSelector.cs | 25 ++- .../Styling/PropertySetterBindingInstance.cs | 48 +++++ .../Styling/PropertySetterInstance.cs | 82 +++++++++ src/Avalonia.Styling/Styling/Selector.cs | 44 +++-- src/Avalonia.Styling/Styling/SelectorMatch.cs | 29 +-- src/Avalonia.Styling/Styling/Setter.cs | 141 ++++++--------- src/Avalonia.Styling/Styling/Style.cs | 165 +++-------------- .../Styling/StyleActivator.cs | 56 ------ src/Avalonia.Styling/Styling/StyleInstance.cs | 81 +++++++++ src/Avalonia.Styling/Styling/Styler.cs | 25 ++- src/Avalonia.Styling/Styling/Styles.cs | 82 ++++----- .../Styling/TypeNameAndClassSelector.cs | 109 ++--------- .../Styling/StyleInclude.cs | 19 +- .../Styling/ApplyStyling.cs | 2 +- .../Styling/StyleAttachBenchmark.cs | 6 +- .../Primitives/TemplatedControlTests.cs | 8 +- .../TabControlTests.cs | 2 +- .../UserControlTests.cs | 2 +- .../AvaloniaPropertyConverterTest.cs | 10 +- .../StyleTests.cs | 37 +--- .../ActivatedObservableTests.cs | 71 -------- .../ActivatedSubjectTests.cs | 92 ---------- .../ActivatedValueTests.cs | 75 -------- .../SelectorTests_Class.cs | 16 +- .../SelectorTests_Descendent.cs | 2 +- .../SelectorTests_Multiple.cs | 29 +++ .../SelectorTests_Not.cs | 8 +- .../SelectorTests_PropertyEquals.cs | 2 +- .../Avalonia.Styling.UnitTests/SetterTests.cs | 51 +++--- .../StyleActivatorExtensions.cs | 42 +++++ .../StyleActivatorTests.cs | 169 ------------------ .../Avalonia.Styling.UnitTests/StyleTests.cs | 58 ++---- .../StyledElementTests.cs | 9 +- 56 files changed, 1166 insertions(+), 1486 deletions(-) delete mode 100644 src/Avalonia.Styling/Styling/ActivatedObservable.cs delete mode 100644 src/Avalonia.Styling/Styling/ActivatedSubject.cs delete mode 100644 src/Avalonia.Styling/Styling/ActivatedValue.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/AndActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/NotActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/OrActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs create mode 100644 src/Avalonia.Styling/Styling/ISetterInstance.cs create mode 100644 src/Avalonia.Styling/Styling/IStyleInstance.cs create mode 100644 src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs create mode 100644 src/Avalonia.Styling/Styling/PropertySetterInstance.cs delete mode 100644 src/Avalonia.Styling/Styling/StyleActivator.cs create mode 100644 src/Avalonia.Styling/Styling/StyleInstance.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs create mode 100644 tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/StyleActivatorTests.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..4eca8a3c25 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -6,6 +6,8 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.VisualTree; @@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels Type = visual.GetType().Name; Visual = visual; - if (visual is IStyleable styleable) + if (visual is IControl control) { + var removed = Observable.FromEventPattern( + x => control.DetachedFromLogicalTree += x, + x => control.DetachedFromLogicalTree -= x); var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x) + .TakeUntil(removed); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { - if (styleable.Classes.Count > 0) + if (control.Classes.Count > 0) { - Classes = "(" + string.Join(" ", styleable.Classes) + ")"; + Classes = "(" + string.Join(" ", control.Classes) + ")"; } else { diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index a396cee35f..b4f6c2c942 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -8,5 +8,4 @@ - diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 354ed33657..51f4c5c4eb 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -1,11 +1,5 @@ using System; -using System.Linq; using System.Reactive.Disposables; -using System.Reactive.Threading.Tasks; -using System.Reflection; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 120a53c664..aeb3b5dc53 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -14,6 +12,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia { /// @@ -29,8 +29,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); @@ -38,34 +38,34 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); /// /// Defines the property. /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); private int _initCount; - private string _name; + private string? _name; private readonly Classes _classes = new Classes(); - private ILogicalRoot _logicalRoot; - private IAvaloniaList _logicalChildren; - private IResourceDictionary _resources; - private Styles _styles; + private ILogicalRoot? _logicalRoot; + private IAvaloniaList? _logicalChildren; + private IResourceDictionary? _resources; + private Styles? _styles; private bool _styled; - private Subject _styleDetach = new Subject(); - private ITemplatedControl _templatedParent; + private List? _appliedStyles; + private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; /// @@ -87,12 +87,12 @@ namespace Avalonia /// /// Raised when the styled element is attached to a rooted logical tree. /// - public event EventHandler AttachedToLogicalTree; + public event EventHandler? AttachedToLogicalTree; /// /// Raised when the styled element is detached from a rooted logical tree. /// - public event EventHandler DetachedFromLogicalTree; + public event EventHandler? DetachedFromLogicalTree; /// /// Occurs when the property changes. @@ -101,7 +101,7 @@ namespace Avalonia /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// - public event EventHandler DataContextChanged; + public event EventHandler? DataContextChanged; /// /// Occurs when the styled element has finished initialization. @@ -114,12 +114,12 @@ namespace Avalonia /// is not used, it is called when the styled element is attached /// to the visual tree. /// - public event EventHandler Initialized; + public event EventHandler? Initialized; /// /// Occurs when a resource in this styled element or a parent styled element has changed. /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets the name of the styled element. @@ -128,20 +128,12 @@ namespace Avalonia /// An element's name is used to uniquely identify an element within the element's name /// scope. Once the element is added to a logical tree, its name cannot be changed. /// - public string Name + public string? Name { - get - { - return _name; - } + get => _name; set { - if (String.IsNullOrWhiteSpace(value)) - { - throw new InvalidOperationException("Cannot set Name to null or empty string."); - } - if (_styled) { throw new InvalidOperationException("Cannot set Name : styled element already styled."); @@ -189,7 +181,7 @@ namespace Avalonia /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -214,28 +206,15 @@ namespace Avalonia /// public Styles Styles { - get { return _styles ?? (Styles = new Styles()); } - set + get { - Contract.Requires(value != null); - - if (_styles != value) + if (_styles is null) { - if (_styles != null) - { - (_styles as ISetResourceParent)?.SetParent(null); - _styles.ResourcesChanged -= ThisResourcesChanged; - } - - _styles = value; - - if (value is ISetResourceParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - + _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; } + + return _styles; } } @@ -247,7 +226,7 @@ namespace Avalonia get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -270,7 +249,7 @@ namespace Avalonia /// /// Gets the styled element whose lookless template this styled element is part of. /// - public ITemplatedControl TemplatedParent + public ITemplatedControl? TemplatedParent { get => _templatedParent; internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); @@ -312,12 +291,12 @@ namespace Avalonia /// /// Gets the styled element's logical parent. /// - public IStyledElement Parent { get; private set; } + public IStyledElement? Parent { get; private set; } /// /// Gets the styled element's logical parent. /// - ILogical ILogical.LogicalParent => Parent; + ILogical? ILogical.LogicalParent => Parent; /// /// Gets the styled element's logical children. @@ -328,7 +307,7 @@ namespace Avalonia bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; /// - IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -344,14 +323,11 @@ namespace Avalonia /// Type IStyleable.StyleKey => GetType(); - /// - IObservable IStyleable.StyleDetach => _styleDetach; - /// bool IStyleHost.IsStylesInitialized => _styles != null; /// - IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; + IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// public virtual void BeginInit() @@ -397,13 +373,13 @@ namespace Avalonia /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnAttachedToLogicalTreeCore(e); + OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnDetachedFromLogicalTreeCore(e); + OnDetachedFromLogicalTreeCore(e); } /// @@ -413,7 +389,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceProvider.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -424,7 +400,7 @@ namespace Avalonia /// Sets the styled element's logical parent. /// /// The parent. - void ISetLogicalParent.SetParent(ILogical parent) + void ISetLogicalParent.SetParent(ILogical? parent) { var old = Parent; @@ -440,7 +416,7 @@ namespace Avalonia InheritanceParent = parent as AvaloniaObject; } - Parent = (IStyledElement)parent; + Parent = (IStyledElement?)parent; if (_logicalRoot != null) { @@ -470,12 +446,13 @@ namespace Avalonia var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } - +#nullable disable RaisePropertyChanged( ParentProperty, new Optional(old), new BindingValue(Parent), BindingPriority.LocalValue); +#nullable enable } } @@ -488,6 +465,16 @@ namespace Avalonia InheritanceParent = parent; } + void IStyleable.StyleApplied(IStyleInstance instance) + { + instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + _appliedStyles ??= new List(); + _appliedStyles.Add(instance); + } + + void IStyleable.DetachStyles() => DetachStyles(); + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -597,7 +584,7 @@ namespace Avalonia } } - private static ILogicalRoot FindLogicalRoot(IStyleHost e) + private static ILogicalRoot? FindLogicalRoot(IStyleHost e) { while (e != null) { @@ -666,7 +653,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; - _styleDetach.OnNext(this); + DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); @@ -682,7 +669,7 @@ namespace Avalonia } #if DEBUG - if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) + if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) { Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, @@ -710,6 +697,19 @@ namespace Avalonia } } + private void DetachStyles() + { + if (_appliedStyles is object) + { + foreach (var i in _appliedStyles) + { + i.Dispose(); + } + + _appliedStyles.Clear(); + } + } + private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs deleted file mode 100644 index 5b2774943a..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Styling -{ - /// - /// An observable which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and a - /// observable which produces the activated value. When the activator - /// produces true, the will produce the current activated - /// value. When the activator produces false it will produce - /// . - /// - internal class ActivatedObservable : ActivatedValue, IDescription - { - private IDisposable _sourceSubscription; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedObservable( - IObservable activator, - IObservable source, - string description) - : base(activator, AvaloniaProperty.UnsetValue, description) - { - Contract.Requires(source != null); - - Source = source; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source { get; } - - protected override ActivatorListener CreateListener() => new ValueListener(this); - - protected override void Deinitialize() - { - base.Deinitialize(); - _sourceSubscription.Dispose(); - _sourceSubscription = null; - } - - protected override void Initialize() - { - base.Initialize(); - _sourceSubscription = Source.Subscribe((ValueListener)Listener); - } - - protected virtual void NotifyValue(object value) - { - Value = value; - } - - private class ValueListener : ActivatorListener, IObserver - { - public ValueListener(ActivatedObservable parent) - : base(parent) - { - } - protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(object value) => Parent.NotifyValue(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs deleted file mode 100644 index a8446c4bfb..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Subjects; - -namespace Avalonia.Styling -{ - /// - /// A subject which is switched on or off according to an activator observable. - /// - /// - /// An extends to - /// be an . When the object is active then values - /// received via will be passed to the source subject. - /// - internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription - { - private bool _completed; - private object _pushValue; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedSubject( - IObservable activator, - ISubject source, - string description) - : base(activator, source, description) - { - } - - /// - /// Gets the underlying subject. - /// - public new ISubject Source - { - get { return (ISubject)base.Source; } - } - - public void OnCompleted() - { - Source.OnCompleted(); - } - - public void OnError(Exception error) - { - Source.OnError(error); - } - - public void OnNext(object value) - { - _pushValue = value; - - if (IsActive == true && !_completed) - { - Source.OnNext(_pushValue); - } - } - - protected override void ActiveChanged(bool active) - { - bool first = !IsActive.HasValue; - - base.ActiveChanged(active); - - if (!first) - { - Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); - } - } - - protected override void CompletedReceived() - { - base.CompletedReceived(); - - if (!_completed) - { - Source.OnCompleted(); - _completed = true; - } - } - - protected override void ErrorReceived(Exception error) - { - base.ErrorReceived(error); - - if (!_completed) - { - Source.OnError(error); - _completed = true; - } - } - - private void ActivatorCompleted() - { - _completed = true; - Source.OnCompleted(); - } - - private void ActivatorError(Exception e) - { - _completed = true; - Source.OnError(e); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs deleted file mode 100644 index 908d89b751..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Reactive; - -namespace Avalonia.Styling -{ - /// - /// An value which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and an - /// . When the activator produces true, the - /// will produce the current value. When the activator - /// produces false it will produce . - /// - internal class ActivatedValue : LightweightObservableBase, IDescription - { - private static readonly object NotSent = new object(); - private IDisposable _activatorSubscription; - private object _value; - private object _last = NotSent; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public ActivatedValue( - IObservable activator, - object value, - string description) - { - Contract.Requires(activator != null); - - Activator = activator; - Value = value; - Description = description; - Listener = CreateListener(); - } - - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - - /// - /// Gets a value indicating whether the activator is active. - /// - public bool? IsActive { get; private set; } - - /// - /// Gets the value that will be produced when is true. - /// - public object Value - { - get => _value; - protected set - { - _value = value; - PublishValue(); - } - } - - protected ActivatorListener Listener { get; } - - protected virtual void ActiveChanged(bool active) - { - IsActive = active; - PublishValue(); - } - - protected virtual void CompletedReceived() => PublishCompleted(); - - protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); - - protected override void Deinitialize() - { - _activatorSubscription.Dispose(); - _activatorSubscription = null; - } - - protected virtual void ErrorReceived(Exception error) => PublishError(error); - - protected override void Initialize() - { - _activatorSubscription = Activator.Subscribe(Listener); - } - - protected override void Subscribed(IObserver observer, bool first) - { - if (IsActive == true && !first) - { - observer.OnNext(Value); - } - } - - private void PublishValue() - { - if (IsActive.HasValue) - { - var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; - - if (!Equals(v, _last)) - { - PublishNext(v); - _last = v; - } - } - } - - protected class ActivatorListener : IObserver - { - public ActivatorListener(ActivatedValue parent) - { - Parent = parent; - } - - protected ActivatedValue Parent { get; } - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivator.cs b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs new file mode 100644 index 0000000000..8ab281e8d0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class AndActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private ulong _mask; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (_mask != 0) + { + PublishNext(_flags == _mask); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _mask = (1ul << Count) - 1; + PublishNext(_flags == _mask); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + + _mask = 0; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs new file mode 100644 index 0000000000..479100ed8a --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System; + +namespace Avalonia.Styling.Activators +{ + /// + /// Defines a style activator. + /// + /// + /// A style activator is very similar to an `IObservable{bool}` but is optimized for the + /// particular use-case of activating a style according to a selector. It differs from + /// an observable in two major ways: + /// + /// - Can only have a single subscription + /// - The subscription can have a tag associated with it, allowing a subscriber to index + /// into a list of subscriptions without having to allocate additional objects. + /// + public interface IStyleActivator : IDisposable + { + /// + /// Subscribes to the activator. + /// + /// The listener. + /// An optional tag. + void Subscribe(IStyleActivatorSink sink, int tag = 0); + + /// + /// Unsubscribes from the activator. + /// + void Unsubscribe(IStyleActivatorSink sink); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs new file mode 100644 index 0000000000..a1a6ef5c28 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Receives notifications from an . + /// + public interface IStyleActivatorSink + { + /// + /// Called when the subscribed activator value changes. + /// + /// The new value. + /// The subscription tag. + void OnNext(bool value, int tag); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NotActivator.cs b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs new file mode 100644 index 0000000000..4c152a8f0f --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class NotActivator : StyleActivatorBase, IStyleActivatorSink + { + private readonly IStyleActivator _source; + public NotActivator(IStyleActivator source) => _source = source; + void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); + protected override void Initialize() => _source.Subscribe(this, 0); + protected override void Deinitialize() => _source.Unsubscribe(this); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivator.cs b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs new file mode 100644 index 0000000000..0220265e10 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class OrActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private bool _initializing; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (!_initializing) + { + PublishNext(_flags != 0); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + _initializing = true; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _initializing = false; + PublishNext(_flags != 0); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs new file mode 100644 index 0000000000..abf3c1717e --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -0,0 +1,35 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class PropertyEqualsActivator : StyleActivatorBase, IObserver + { + private readonly IStyleable _control; + private readonly AvaloniaProperty _property; + private readonly object? _value; + private IDisposable? _subscription; + + public PropertyEqualsActivator( + IStyleable control, + AvaloniaProperty property, + object? value) + { + _control = control ?? throw new ArgumentNullException(nameof(control)); + _property = property ?? throw new ArgumentNullException(nameof(property)); + _value = value; + } + + protected override void Initialize() + { + _subscription = _control.GetObservable(_property).Subscribe(this); + } + + protected override void Deinitialize() => _subscription?.Dispose(); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs new file mode 100644 index 0000000000..725547ed05 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs @@ -0,0 +1,55 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal abstract class StyleActivatorBase : IStyleActivator + { + private IStyleActivatorSink? _sink; + private int _tag; + private bool? _value; + + public void Subscribe(IStyleActivatorSink sink, int tag = 0) + { + if (_sink is null) + { + _sink = sink; + _tag = tag; + _value = null; + Initialize(); + } + else + { + throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); + } + } + + public void Unsubscribe(IStyleActivatorSink sink) + { + if (_sink != sink) + { + throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); + } + + _sink = null; + Deinitialize(); + } + + public void PublishNext(bool value) + { + if (_value != value) + { + _value = value; + _sink?.OnNext(value, _tag); + } + } + + public void Dispose() + { + _sink = null; + Deinitialize(); + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs new file mode 100644 index 0000000000..906a8303cb --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal sealed class StyleClassActivator : StyleActivatorBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + + public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) + { + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + + protected override void Initialize() + { + PublishNext(IsMatching()); + _classes.CollectionChanged += ClassesChanged; + } + + protected override void Deinitialize() + { + _classes.CollectionChanged -= ClassesChanged; + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => AreClassesMatching(_classes, _match); + } +} diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs index a81908f23d..08b25f4057 100644 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ b/src/Avalonia.Styling/Styling/DescendentSelector.cs @@ -2,24 +2,21 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { internal class DescendantSelector : Selector { private readonly Selector _parent; - private string _selectorString; + private string? _selectorString; - public DescendantSelector(Selector parent) + public DescendantSelector(Selector? parent) { - if (parent == null) - { - throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); - } - - _parent = parent; + _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); } /// @@ -29,7 +26,7 @@ namespace Avalonia.Styling public override bool InTemplate => _parent.InTemplate; /// - public override Type TargetType => null; + public override Type? TargetType => null; public override string ToString() { @@ -43,8 +40,9 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - ILogical c = (ILogical)control; - List> descendantMatches = new List>(); + var c = (ILogical)control; + IStyleActivator? descendentMatch = null; + OrActivator? descendantMatches = null; while (c != null) { @@ -56,7 +54,21 @@ namespace Avalonia.Styling if (match.Result == SelectorMatchResult.Sometimes) { - descendantMatches.Add(match.Activator); + if (descendentMatch is null && descendantMatches is null) + { + descendentMatch = match.Activator; + } + else + { + if (descendantMatches is null) + { + descendantMatches = new OrActivator(); + descendantMatches.Add(descendentMatch!); + descendentMatch = null; + } + + descendantMatches.Add(match.Activator!); + } } else if (match.IsMatch) { @@ -65,9 +77,13 @@ namespace Avalonia.Styling } } - if (descendantMatches.Count > 0) + if (descendantMatches is object) + { + return new SelectorMatch(descendantMatches); + } + else if (descendentMatch is object) { - return new SelectorMatch(StyleActivator.Or(descendantMatches)); + return new SelectorMatch(descendentMatch); } else { @@ -75,6 +91,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; } } diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Styling/Styling/ISetter.cs index da97638f07..44e43caf85 100644 --- a/src/Avalonia.Styling/Styling/ISetter.cs +++ b/src/Avalonia.Styling/Styling/ISetter.cs @@ -3,6 +3,8 @@ using System; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,16 @@ namespace Avalonia.Styling public interface ISetter { /// - /// Applies the setter to a control. + /// Instances a setter on a control. /// - /// The style that is being applied. - /// The control. - /// An optional activator. - IDisposable Apply(IStyle style, IStyleable control, IObservable activator); + /// The control. + /// Whether the parent style has an activator. + /// An . + /// + /// This method should return an which can be used to apply + /// the setter to the specified control. Note that it should not apply the setter value + /// until is called. + /// + ISetterInstance Instance(IStyleable target, bool hasActivator); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Styling/Styling/ISetterInstance.cs new file mode 100644 index 0000000000..ebfc227d12 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetterInstance.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a setter that has been instanced on a control. + /// + public interface ISetterInstance + { + /// + /// Activates the setter. + /// + public void Activate(); + + /// + /// Deactivates the setter. + /// + public void Deactivate(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index da2a08f04d..8151aacf54 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -13,17 +15,11 @@ namespace Avalonia.Styling /// /// Attaches the style to a control if the style's selector matches. /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// + /// The control to attach to. + /// The element that hosts the style. /// - /// True if the style can match a control of type - /// (even if it does not match this control specifically); false if the style - /// can never match. + /// A describing how the style matches the control. /// - bool Attach(IStyleable control, IStyleHost container); - - void Detach(); + SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); } } diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs new file mode 100644 index 0000000000..cb094badd2 --- /dev/null +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a style that has been instanced on a control. + /// + public interface IStyleInstance : IDisposable + { + /// + /// Gets the source style. + /// + IStyle Source { get; } + + /// + /// Instructs the style to start acting upon the control. + /// + void Start(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index 5ad97d8a61..b01c779bcc 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -4,6 +4,8 @@ using System; using Avalonia.Collections; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,6 @@ namespace Avalonia.Styling /// public interface IStyleable : IAvaloniaObject, INamed { - /// - /// Signaled when the control's style should be removed. - /// - IObservable StyleDetach { get; } - /// /// Gets the list of classes for the control. /// @@ -29,6 +26,17 @@ namespace Avalonia.Styling /// /// Gets the template parent of this element if the control comes from a template. /// - ITemplatedControl TemplatedParent { get; } + ITemplatedControl? TemplatedParent { get; } + + /// + /// Notifies the element that a style has been applied. + /// + /// The style instance. + void StyleApplied(IStyleInstance instance); + + /// + /// Detaches all styles applied to the element. + /// + void DetachStyles(); } } diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs index bcf76620be..6428535a12 100644 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -11,16 +13,16 @@ namespace Avalonia.Styling /// internal class NotSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Selector _argument; - private string _selectorString; + private string? _selectorString; /// /// Initializes a new instance of the class. /// /// The previous selector. /// The selector to be not-ed. - public NotSelector(Selector previous, Selector argument) + public NotSelector(Selector? previous, Selector argument) { _previous = previous; _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); @@ -33,7 +35,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -61,12 +63,12 @@ namespace Avalonia.Styling case SelectorMatchResult.NeverThisType: return SelectorMatch.AlwaysThisType; case SelectorMatchResult.Sometimes: - return new SelectorMatch(innerResult.Activator.Select(x => !x)); + return new SelectorMatch(new NotActivator(innerResult.Activator!)); default: throw new InvalidOperationException("Invalid SelectorMatchResult."); } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs index 58c5c778fb..9c76a38f45 100644 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ b/src/Avalonia.Styling/Styling/OrSelector.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -12,8 +15,8 @@ namespace Avalonia.Styling internal class OrSelector : Selector { private readonly IReadOnlyList _selectors; - private string _selectorString; - private Type _targetType; + private string? _selectorString; + private Type? _targetType; /// /// Initializes a new instance of the class. @@ -21,8 +24,15 @@ namespace Avalonia.Styling /// The selectors to OR. public OrSelector(IReadOnlyList selectors) { - Contract.Requires(selectors != null); - Contract.Requires(selectors.Count > 1); + if (selectors is null) + { + throw new ArgumentNullException(nameof(selectors)); + } + + if (selectors.Count <= 1) + { + throw new ArgumentException("Need more than one selector to OR."); + } _selectors = selectors; } @@ -34,7 +44,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType + public override Type? TargetType { get { @@ -60,7 +70,8 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var activators = new List>(); + IStyleActivator? activator = null; + OrActivator? activators = null; var neverThisInstance = false; foreach (var selector in _selectors) @@ -76,18 +87,32 @@ namespace Avalonia.Styling neverThisInstance = true; break; case SelectorMatchResult.Sometimes: - activators.Add(match.Activator); + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new OrActivator(); + activators.Add(activator!); + activator = null; + } + + activators.Add(match.Activator!); + } break; } } - if (activators.Count > 1) + if (activators is object) { - return new SelectorMatch(StyleActivator.Or(activators)); + return new SelectorMatch(activators); } - else if (activators.Count == 1) + else if (activator is object) { - return new SelectorMatch(activators[0]); + return new SelectorMatch(activator); } else if (neverThisInstance) { @@ -99,11 +124,11 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; - private Type EvaluateTargetType() + private Type? EvaluateTargetType() { - var result = default(Type); + Type? result = null; foreach (var selector in _selectors) { diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cfc0998fe0..d7e1f46a94 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Text; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -13,14 +15,14 @@ namespace Avalonia.Styling /// internal class PropertyEqualsSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly AvaloniaProperty _property; - private readonly object _value; - private string _selectorString; + private readonly object? _value; + private string? _selectorString; - public PropertyEqualsSelector(Selector previous, AvaloniaProperty property, object value) + public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); _previous = previous; _property = property; @@ -33,13 +35,8 @@ namespace Avalonia.Styling /// public override bool IsCombinator => false; - /// - /// Gets the name of the control to match. - /// - public string Name { get; private set; } - /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -77,7 +74,7 @@ namespace Avalonia.Styling { if (subscribe) { - return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); + return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value)); } else { @@ -86,6 +83,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs new file mode 100644 index 0000000000..74d7f98398 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterBindingInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly AvaloniaProperty _property; + private readonly BindingPriority _priority; + private readonly InstancedBinding _binding; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterBindingInstance( + IStyleable target, + AvaloniaProperty property, + BindingPriority priority, + IBinding binding) + { + _target = target; + _property = property; + _priority = priority; + _binding = binding.Initiate(target, property).WithPriority(priority); + } + + public void Activate() + { + if (!_isActive) + { + _subscription = BindingOperations.Apply(_target, _property, _binding, null); + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + _subscription?.Dispose(); + _subscription = null; + _isActive = false; + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs new file mode 100644 index 0000000000..284ca8cdd0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly StyledPropertyBase? _styledProperty; + private readonly DirectPropertyBase? _directProperty; + private readonly BindingPriority _priority; + private readonly T _value; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterInstance( + IStyleable target, + StyledPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _styledProperty = property; + _priority = priority; + _value = value; + } + + public PropertySetterInstance( + IStyleable target, + DirectPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _directProperty = property; + _priority = priority; + _value = value; + } + + public void Activate() + { + if (!_isActive) + { + if (_styledProperty is object) + { + _subscription = _target.SetValue(_styledProperty, _value, _priority); + } + else + { + _target.SetValue(_directProperty!, _value); + } + + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + if (_subscription is null) + { + if (_styledProperty is object) + { + _target.ClearValue(_styledProperty); + } + else + { + _target.ClearValue(_directProperty!); + } + } + else + { + _subscription.Dispose(); + _subscription = null; + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index 7d4e92baeb..6d74eb8842 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Diagnostics; -using Avalonia.Utilities; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -30,7 +30,7 @@ namespace Avalonia.Styling /// /// Gets the target type of the selector, if available. /// - public abstract Type TargetType { get; } + public abstract Type? TargetType { get; } /// /// Tries to match the selector with a control. @@ -43,8 +43,8 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - ValueSingleOrList> inputs = default; - + IStyleActivator? activator = null; + AndActivator? activators = null; var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -69,21 +69,39 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { - Debug.Assert(match.Activator != null); + if (match.Activator is null) + { + throw new AvaloniaInternalException( + "SelectorMatch returned Sometimes but there is no activator."); + } + + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new AndActivator(); + activators.Add(activator!); + activator = null; + } - inputs.Add(match.Activator); + activators.Add(match.Activator); + } } selector = selector.MovePrevious(); } - if (inputs.HasList) + if (activators is object) { - return new SelectorMatch(StyleActivator.And(inputs.List)); + return new SelectorMatch(activators); } - else if (inputs.IsSingle) + else if (activator is object) { - return new SelectorMatch(inputs.Single); + return new SelectorMatch(activator); } else { @@ -107,6 +125,6 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector MovePrevious(); + protected abstract Selector? MovePrevious(); } } diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Styling/Styling/SelectorMatch.cs index 63b89e9e97..3cc84a0b57 100644 --- a/src/Avalonia.Styling/Styling/SelectorMatch.cs +++ b/src/Avalonia.Styling/Styling/SelectorMatch.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -21,9 +24,9 @@ namespace Avalonia.Styling NeverThisInstance, /// - /// The selector always matches this type. + /// The selector matches this instance based on the . /// - AlwaysThisType, + Sometimes, /// /// The selector always matches this instance, but doesn't always match this type. @@ -31,9 +34,9 @@ namespace Avalonia.Styling AlwaysThisInstance, /// - /// The selector matches this instance based on the . + /// The selector always matches this type. /// - Sometimes, + AlwaysThisType, } /// @@ -43,7 +46,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public class SelectorMatch + public readonly struct SelectorMatch { /// /// A selector match with the result of . @@ -70,20 +73,24 @@ namespace Avalonia.Styling /// result. /// /// The match activator. - public SelectorMatch(IObservable match) + public SelectorMatch(IStyleActivator match) { - Contract.Requires(match != null); + match = match ?? throw new ArgumentNullException(nameof(match)); Result = SelectorMatchResult.Sometimes; Activator = match; } - private SelectorMatch(SelectorMatchResult result) => Result = result; + private SelectorMatch(SelectorMatchResult result) + { + Result = result; + Activator = null; + } /// /// Gets a value indicating whether the match was positive. /// - public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType; + public bool IsMatch => Result >= SelectorMatchResult.Sometimes; /// /// Gets the result of the match. @@ -91,9 +98,9 @@ namespace Avalonia.Styling public SelectorMatchResult Result { get; } /// - /// Gets an observable which tracks the selector match, in the case of selectors that can + /// Gets an activator which tracks the selector match, in the case of selectors that can /// change over time. /// - public IObservable Activator { get; } + public IStyleActivator? Activator { get; } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index b880ecb01c..08e8a699b8 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -2,11 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; using Avalonia.Animation; using Avalonia.Data; using Avalonia.Metadata; -using Avalonia.Reactive; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.Styling { @@ -17,9 +18,9 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor { - private object _value; + private object? _value; /// /// Initializes a new instance of the class. @@ -42,11 +43,7 @@ namespace Avalonia.Styling /// /// Gets or sets the property to set. /// - public AvaloniaProperty Property - { - get; - set; - } + public AvaloniaProperty? Property { get; set; } /// /// Gets or sets the property value. @@ -54,13 +51,9 @@ namespace Avalonia.Styling [Content] [AssignBinding] [DependsOn(nameof(Property))] - public object Value + public object? Value { - get - { - return _value; - } - + get => _value; set { (value as ISetterValue)?.Initialize(this); @@ -68,99 +61,71 @@ namespace Avalonia.Styling } } - /// - /// Applies the setter to a control. - /// - /// The style that is being applied. - /// The control. - /// An optional activator. - public IDisposable Apply(IStyle style, IStyleable control, IObservable activator) + public ISetterInstance Instance(IStyleable target, bool hasActivator) { - Contract.Requires(control != null); + target = target ?? throw new ArgumentNullException(nameof(target)); - if (Property == null) + if (Property is null) { throw new InvalidOperationException("Setter.Property must be set."); } - var value = Value; - var binding = value as IBinding; + var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - if (binding == null) + if (Value is IBinding binding) { - if (value is ITemplate template) - { - bool isPropertyOfTypeITemplate = typeof(ITemplate).IsAssignableFrom(Property.PropertyType); - - if (!isPropertyOfTypeITemplate) - { - var materialized = template.Build(); - value = materialized; - } - } - - if (activator == null) - { - return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style); - } - else - { - var description = style?.ToString(); - - var activated = new ActivatedValue(activator, value, description); - return control.Bind(Property, activated, BindingPriority.StyleTrigger); - } + return new PropertySetterBindingInstance(target, Property, priority, binding); } else { - var source = binding.Initiate(control, Property); + var value = Value; - if (source != null) + if (value is ITemplate template && + !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) { - var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); - return BindingOperations.Apply(control, Property, cloned, null); + value = template.Build(); } - } - return Disposable.Empty; + var data = new SetterVisitorData + { + target = target, + priority = priority, + value = value, + }; + + Property.Accept(this, ref data); + return data.result!; + } } - private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) + void IAvaloniaPropertyVisitor.Visit( + StyledPropertyBase property, + ref SetterVisitorData data) { - if (activator != null) - { - var description = style?.ToString(); + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - switch (mode) - { - case BindingMode.OneTime: - if (sourceInstance.Observable != null) - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - else - { - var activated = new ActivatedValue(activator, sourceInstance.Value, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - case BindingMode.OneWay: - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); - } - default: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); - } - } + void IAvaloniaPropertyVisitor.Visit( + DirectPropertyBase property, + ref SetterVisitorData data) + { + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - } - else - { - return sourceInstance.WithPriority(BindingPriority.Style); - } + private struct SetterVisitorData + { + public IStyleable target; + public BindingPriority priority; + public object? value; + public ISetterInstance? result; } } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 22db7adfe4..c607ee60e5 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,15 +16,10 @@ namespace Avalonia.Styling /// public class Style : AvaloniaObject, IStyle, ISetResourceParent { - private static Dictionary _applied = - new Dictionary(); - private IResourceNode _parent; - - private CompositeDisposable _subscriptions; - - private IResourceDictionary _resources; - - private IList _animations; + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; /// /// Initializes a new instance of the class. @@ -37,13 +32,13 @@ namespace Avalonia.Styling /// Initializes a new instance of the class. /// /// The style selector. - public Style(Func selector) + public Style(Func selector) { Selector = selector(null); } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets a dictionary of style resources. @@ -53,7 +48,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -76,117 +71,45 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector Selector { get; set; } + public Selector? Selector { get; set; } /// - /// Gets or sets the style's setters. + /// Gets the style's setters. /// [Content] - public IList Setters { get; set; } = new List(); + public IList Setters => _setters ??= new List(); - public IList Animations - { - get - { - return _animations ?? (_animations = new List()); - } - } - - private CompositeDisposable Subscriptions - { - get - { - return _subscriptions ?? (_subscriptions = new CompositeDisposable(2)); - } - } + /// + /// Gets the style's animations. + /// + public IList Animations => _animations ??= new List(); /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool IResourceProvider.HasResources => _resources?.Count > 0; /// - public bool Attach(IStyleable control, IStyleHost container) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (Selector != null) - { - var match = Selector.Match(control); - - if (match.IsMatch) - { - var controlSubscriptions = GetSubscriptions(control); - - var animatable = control as Animatable; - - var setters = Setters; - var settersCount = setters.Count; - var animations = Animations; - var animationsCount = animations.Count; - - var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); - - if (animatable != null) - { - for (var i = 0; i < animationsCount; i++) - { - var animation = animations[i]; - var obsMatch = match.Activator; - - if (match.Result == SelectorMatchResult.AlwaysThisType || - match.Result == SelectorMatchResult.AlwaysThisInstance) - { - obsMatch = Observable.Return(true); - } - - var sub = animation.Apply(animatable, null, obsMatch); - subs.Add(sub); - } - } - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, match.Activator); - subs.Add(sub); - } + target = target ?? throw new ArgumentNullException(nameof(target)); - subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs))); + var match = Selector is object ? Selector.Match(target) : + target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - } - - return match.Result != SelectorMatchResult.NeverThisType; - } - else if (control == container) + if (match.IsMatch && _setters is object) { - var setters = Setters; - var settersCount = setters.Count; - - var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(settersCount + 1); - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, null); - subs.Add(sub); - } - - subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs))); - - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - return true; + var instance = new StyleInstance(this, target, _setters, match.Activator); + target.StyleApplied(instance); + instance.Start(); } - return false; + return match.Result; } /// - public bool TryGetResource(object key, out object result) + public bool TryGetResource(object key, out object? result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; @@ -224,44 +147,12 @@ namespace Avalonia.Styling if (parent == null) { - Detach(); + //Detach(); } _parent = parent; } - public void Detach() - { - _subscriptions?.Dispose(); - _subscriptions = null; - } - - private static CompositeDisposable GetSubscriptions(IStyleable control) - { - if (!_applied.TryGetValue(control, out var subscriptions)) - { - subscriptions = new CompositeDisposable(3); - subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); - _applied.Add(control, subscriptions); - } - - return subscriptions; - } - - /// - /// Called when a control's is signaled to remove - /// all applied styles. - /// - /// The control. - private static void ControlDetach(IStyleable control) - { - var subscriptions = _applied[control]; - - subscriptions.Dispose(); - - _applied.Remove(control); - } - private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs deleted file mode 100644 index 63945037d8..0000000000 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Styling -{ - public enum ActivatorMode - { - And, - Or, - } - - public static class StyleActivator - { - public static IObservable And(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.And inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.All(x => x)) - .DistinctUntilChanged(); - } - } - - public static IObservable Or(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.Or inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs new file mode 100644 index 0000000000..6977f19f59 --- /dev/null +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class StyleInstance : IStyleInstance, IStyleActivatorSink + { + private readonly List _setters; + private readonly IStyleActivator? _activator; + private bool _active; + + public StyleInstance( + IStyle source, + IStyleable target, + IReadOnlyList setters, + IStyleActivator? activator = null) + { + setters = setters ?? throw new ArgumentNullException(nameof(setters)); + + Source = source ?? throw new ArgumentNullException(nameof(source)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + _setters = new List(setters.Count); + _activator = activator; + + foreach (var setter in setters) + { + _setters.Add(setter.Instance(target, activator is object)); + } + } + + public IStyle Source { get; } + public IStyleable Target { get; } + + public void Start() + { + if (_activator == null) + { + ActivatorChanged(true); + } + else + { + _activator.Subscribe(this, 0); + } + } + + public void Dispose() + { + ActivatorChanged(false); + _activator?.Dispose(); + } + + private void ActivatorChanged(bool value) + { + if (_active != value) + { + _active = value; + + if (_active) + { + foreach (var setter in _setters) + { + setter.Activate(); + } + } + else + { + foreach (var setter in _setters) + { + setter.Deactivate(); + } + } + } + } + + void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); + } +} diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Styling/Styling/Styler.cs index 7ac5c89005..cfd9f65aee 100644 --- a/src/Avalonia.Styling/Styling/Styler.cs +++ b/src/Avalonia.Styling/Styling/Styler.cs @@ -3,35 +3,34 @@ using System; +#nullable enable + namespace Avalonia.Styling { public class Styler : IStyler { - public void ApplyStyles(IStyleable control) + public void ApplyStyles(IStyleable target) { - var styleHost = control as IStyleHost; + target = target ?? throw new ArgumentNullException(nameof(target)); - if (styleHost != null) + if (target is IStyleHost styleHost) { - ApplyStyles(control, styleHost); + ApplyStyles(target, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost styleHost) + private void ApplyStyles(IStyleable target, IStyleHost host) { - Contract.Requires(control != null); - Contract.Requires(styleHost != null); - - var parentContainer = styleHost.StylingParent; + var parent = host.StylingParent; - if (parentContainer != null) + if (parent != null) { - ApplyStyles(control, parentContainer); + ApplyStyles(target, parent); } - if (styleHost.IsStylesInitialized) + if (host.IsStylesInitialized) { - styleHost.Styles.Attach(control, styleHost); + host.Styles.TryAttach(target, host); } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index fd38c39650..fc579266e8 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -9,6 +9,8 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,10 +18,10 @@ namespace Avalonia.Styling /// public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { - private IResourceNode _parent; - private IResourceDictionary _resources; - private AvaloniaList _styles = new AvaloniaList(); - private Dictionary> _cache; + private readonly AvaloniaList _styles = new AvaloniaList(); + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private Dictionary?>? _cache; public Styles() { @@ -60,6 +62,12 @@ namespace Avalonia.Styling () => { }); } + public Styles(IResourceNode parent) + : this() + { + _parent = parent; + } + public event NotifyCollectionChangedEventHandler CollectionChanged { add => _styles.CollectionChanged += value; @@ -67,7 +75,7 @@ namespace Avalonia.Styling } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// public int Count => _styles.Count; @@ -83,7 +91,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(Resources)); var hadResources = false; @@ -104,7 +112,7 @@ namespace Avalonia.Styling } /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool ICollection.IsReadOnly => false; @@ -119,66 +127,50 @@ namespace Avalonia.Styling set => _styles[index] = value; } - /// - /// Attaches the style to a control if the style's selector matches. - /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// - public bool Attach(IStyleable control, IStyleHost container) + /// + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (_cache == null) - { - _cache = new Dictionary>(); - } + _cache ??= new Dictionary?>(); - if (_cache.TryGetValue(control.StyleKey, out var cached)) + if (_cache.TryGetValue(target.StyleKey, out var cached)) { - if (cached != null) + if (cached is object) { foreach (var style in cached) { - style.Attach(control, container); + style.TryAttach(target, host); } - return true; + return SelectorMatchResult.AlwaysThisType; + } + else + { + return SelectorMatchResult.NeverThisType; } - - return false; } else { - List result = null; + List? matches = null; - foreach (var style in this) + foreach (var child in this) { - if (style.Attach(control, container)) + if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) { - if (result == null) - { - result = new List(); - } - - result.Add(style); + matches ??= new List(); + matches.Add(child); } } - _cache.Add(control.StyleKey, result); - return result != null; - } - } - - public void Detach() - { - foreach (IStyle style in this) - { - style.Detach(); + _cache.Add(target.StyleKey, matches); + + return matches is null ? + SelectorMatchResult.NeverThisType : + SelectorMatchResult.AlwaysThisType; } } /// - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, out object? value) { if (_resources != null && _resources.TryGetResource(key, out value)) { diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 401fa54fb5..71b8828cba 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -3,11 +3,10 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Reflection; using System.Text; -using Avalonia.Collections; -using Avalonia.Reactive; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -17,13 +16,12 @@ namespace Avalonia.Styling /// internal class TypeNameAndClassSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Lazy> _classes = new Lazy>(() => new List()); - private Type _targetType; - - private string _selectorString; + private Type? _targetType; + private string? _selectorString; - public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) + public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -32,7 +30,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector Is(Selector previous, Type targetType) + public static TypeNameAndClassSelector Is(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -41,7 +39,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForName(Selector previous, string name) + public static TypeNameAndClassSelector ForName(Selector? previous, string name) { var result = new TypeNameAndClassSelector(previous); result.Name = name; @@ -49,7 +47,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForClass(Selector previous, string className) + public static TypeNameAndClassSelector ForClass(Selector? previous, string className) { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); @@ -57,7 +55,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector previous) + protected TypeNameAndClassSelector(Selector? previous) { _previous = previous; } @@ -68,10 +66,10 @@ namespace Avalonia.Styling /// /// Gets the name of the control to match. /// - public string Name { get; set; } + public string? Name { get; set; } /// - public override Type TargetType => _targetType ?? _previous?.TargetType; + public override Type? TargetType => _targetType ?? _previous?.TargetType; /// public override bool IsCombinator => false; @@ -130,12 +128,12 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new ClassObserver(control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes.Value); return new SelectorMatch(observable); } - if (!AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -144,7 +142,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; private string BuildSelectorString() { @@ -190,80 +188,5 @@ namespace Avalonia.Styling return builder.ToString(); } - - private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) - { - int remainingMatches = toMatch.Count; - int classesCount = classes.Count; - - // Early bail out - we can't match if control does not have enough classes. - if (classesCount < remainingMatches) - { - return false; - } - - for (var i = 0; i < classesCount; i++) - { - var c = classes[i]; - - if (toMatch.Contains(c)) - { - --remainingMatches; - - // Already matched so we can skip checking other classes. - if (remainingMatches == 0) - { - break; - } - } - } - - return remainingMatches == 0; - } - - private sealed class ClassObserver : LightweightObservableBase - { - private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private bool _hasMatch; - - public ClassObserver(IAvaloniaReadOnlyList classes, IList match) - { - _classes = classes; - _match = match; - } - - protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - - protected override void Initialize() - { - _hasMatch = IsMatching(); - _classes.CollectionChanged += ClassesChanged; - } - - protected override void Subscribed(IObserver observer, bool first) - { - observer.OnNext(_hasMatch); - } - - private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action != NotifyCollectionChangedAction.Move) - { - var hasMatch = IsMatching(); - - if (hasMatch != _hasMatch) - { - PublishNext(hasMatch); - _hasMatch = hasMatch; - } - } - } - - private bool IsMatching() - { - return AreClassesMatching(_classes, _match); - } - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 41eab79ed8..e5e28b344f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -4,6 +4,7 @@ using Avalonia.Styling; using System; using Avalonia.Controls; +using System.Collections.Generic; namespace Avalonia.Markup.Xaml.Styling { @@ -67,23 +68,7 @@ namespace Avalonia.Markup.Xaml.Styling IResourceNode IResourceNode.ResourceParent => _parent; /// - public bool Attach(IStyleable control, IStyleHost container) - { - if (Source != null) - { - return Loaded.Attach(control, container); - } - - return false; - } - - public void Detach() - { - if (Source != null) - { - Loaded.Detach(); - } - } + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host); /// public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index d24a646f74..b9d0b53728 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling { _window.Styles.Add(new Style(x => x.OfType().Class("foo").Class("bar").Class("baz")) { - Setters = new[] + Setters = { new Setter(TextBox.TextProperty, "foo"), } diff --git a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs index 7bccd65c81..7dad517e51 100644 --- a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Styling; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -34,9 +35,8 @@ namespace Avalonia.Benchmarks.Styling { var styles = UnitTestApplication.Current.Styles; - styles.Attach(_control, UnitTestApplication.Current); - - styles.Detach(); + styles.TryAttach(_control, UnitTestApplication.Current); + ((IStyleable)_control).DetachStyles(); } public void Dispose() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index d36d0b609b..53b5b87ea2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -363,7 +363,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -399,7 +399,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -458,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index a9e86d71ee..6101e7b3d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -163,7 +163,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, template) } diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 9d3e568582..fddef4ec88 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, GetTemplate()) } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index d82300b964..ec9a6ba77f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -139,7 +139,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - IObservable IStyleable.StyleDetach { get; } + public void DetachStyles() + { + throw new NotImplementedException(); + } + + public void StyleApplied(IStyleInstance instance) + { + throw new NotImplementedException(); + } } private class AttachedOwner diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs index 2dc6c4a7fb..44a40af93d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs @@ -53,42 +53,7 @@ namespace Avalonia.Markup.Xaml.UnitTests } }; - setter.Apply(null, control, null); - Assert.Equal("foo", control.Text); - - control.Text = "bar"; - Assert.Equal("bar", data.Foo); - } - } - - [Fact] - public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source() - { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) - { - var data = new Data - { - Foo = "foo", - }; - - var control = new TextBox - { - DataContext = data, - }; - - var setter = new Setter - { - Property = TextBox.TextProperty, - Value = new Binding - { - Path = "Foo", - Mode = BindingMode.TwoWay - } - }; - - var activator = Observable.Never().StartWith(true); - - setter.Apply(null, control, activator); + setter.Instance(control, false).Activate(); Assert.Equal("foo", control.Text); control.Text = "bar"; diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs deleted file mode 100644 index 7773d4767a..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedObservableTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - source.OnNext(2); - activator.OnNext(false); - source.OnNext(3); - activator.OnNext(true); - - Assert.Equal( - new[] - { - AvaloniaProperty.UnsetValue, - 1, - 2, - AvaloniaProperty.UnsetValue, - 3, - }, - result); - } - - [Fact] - public void Should_Complete_When_Source_Completes() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - source.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Source_Errors() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - source.OnError(error); - - Assert.True(completed); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs deleted file mode 100644 index 03f91d97a1..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedSubjectTests - { - [Fact] - public void Should_Set_Values() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - target.OnNext("bar"); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - activator.OnNext(true); - target.OnNext("baz"); - Assert.Equal("baz", source.Value); - activator.OnNext(false); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - target.OnNext("bax"); - activator.OnNext(true); - Assert.Equal("bax", source.Value); - } - - [Fact] - public void Should_Invoke_OnCompleted_On_Activator_Completed() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - activator.OnCompleted(); - - Assert.True(source.Completed); - } - - [Fact] - public void Should_Invoke_OnError_On_Activator_Error() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - var targetError = default(Exception); - var error = new Exception(); - - target.Subscribe(_ => { }, e => targetError = e); - activator.OnError(error); - - Assert.Same(error, source.Error); - Assert.Same(error, targetError); - } - - private class TestSubject : ISubject - { - private IObserver _observer; - - public bool Completed { get; set; } - public Exception Error { get; set; } - public object Value { get; set; } = AvaloniaProperty.UnsetValue; - - public void OnCompleted() - { - Completed = true; - } - - public void OnError(Exception error) - { - Error = error; - } - - public void OnNext(object value) - { - Value = value; - } - - public IDisposable Subscribe(IObserver observer) - { - _observer = observer; - return Disposable.Empty; - } - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs deleted file mode 100644 index 92a7c1bd1f..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedValueTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - activator.OnNext(false); - - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, 1, AvaloniaProperty.UnsetValue }, result); - } - - [Fact] - public void Should_Complete_When_Activator_Completes() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Activator_Errors() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - activator.OnError(error); - - Assert.True(completed); - } - - [Fact] - public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() - { - var scheduler = new TestScheduler(); - var activator1 = scheduler.CreateColdObservable(); - var activator2 = scheduler.CreateColdObservable(); - var activator = StyleActivator.And(new[] { activator1, activator2 }); - var target = new ActivatedValue(activator, 1, string.Empty); - - var subscription = target.Subscribe(_ => { }); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(Subscription.Infinite, activator1.Subscriptions[0].Unsubscribe); - - subscription.Dispose(); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(0, activator1.Subscriptions[0].Unsubscribe); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index fd25b17ba4..00b90f1239 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -36,7 +36,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); @@ -51,7 +51,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "bar" }, + Classes = { "bar" }, }; var target = default(Selector).Class("foo"); @@ -66,7 +66,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, TemplatedParent = new Mock().Object, }; @@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests var control = new Control1(); var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -95,11 +95,11 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.True(await activator.Take(1)); control.Classes.Remove("foo"); @@ -111,7 +111,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1(); var target = default(Selector).Class("foo").Class("bar"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -128,7 +128,7 @@ namespace Avalonia.Styling.UnitTests // Test for #1698 var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 099562b1cf..1128120824 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling.UnitTests child.LogicalParent = parent; var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); - var activator = selector.Match(child).Activator; + var activator = selector.Match(child).Activator.ToObservable(); Assert.False(await activator.Take(1)); parent.Classes.Add("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs index e8be44ed3b..a1ced14108 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs @@ -85,6 +85,35 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); } + [Fact] + public void Control_With_Class_Descendent_Of_Control_With_Two_Classes() + { + var textBlock = new TextBlock(); + var control = new Button { Content = textBlock }; + + control.ApplyTemplate(); + + var selector = default(Selector) + .OfType