Browse Source

Merge branch 'master' into master

Steven Kirk 5 years ago
parent
commit
6210cb6090
100 changed files with 2479 additions and 1445 deletions
  1. 5 0
      .ncrunch/BindingDemo.v3.ncrunchproject
  2. 5 0
      .ncrunch/RenderDemo.v3.ncrunchproject
  3. 5 0
      .ncrunch/VirtualizationDemo.v3.ncrunchproject
  4. 7 7
      azure-pipelines.yml
  5. 3 0
      global.json
  6. 80 86
      nukebuild/Build.cs
  7. 9 12
      nukebuild/BuildParameters.cs
  8. 4 4
      nukebuild/Shims.cs
  9. 4 4
      nukebuild/_build.csproj
  10. 3 3
      packages/Avalonia/Avalonia.csproj
  11. 1 1
      packages/Avalonia/Avalonia.props
  12. 1 1
      samples/BindingDemo/BindingDemo.csproj
  13. 1 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  14. 4 2
      samples/ControlCatalog/MainWindow.xaml
  15. 9 5
      samples/ControlCatalog/MainWindow.xaml.cs
  16. 2 2
      samples/ControlCatalog/Pages/MenuPage.xaml
  17. 1 1
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  18. 1 1
      samples/Previewer/Previewer.csproj
  19. 1 1
      samples/RemoteDemo/RemoteDemo.csproj
  20. 1 1
      samples/RenderDemo/RenderDemo.csproj
  21. 1 1
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  22. 7 2
      src/Avalonia.Base/AvaloniaObject.cs
  23. 11 6
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  24. 13 1
      src/Avalonia.Base/AvaloniaProperty.cs
  25. 10 1
      src/Avalonia.Base/DirectPropertyBase.cs
  26. 1 1
      src/Avalonia.Base/IAvaloniaObject.cs
  27. 8 3
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  28. 7 2
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  29. 1 1
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  30. 11 2
      src/Avalonia.Base/StyledPropertyBase.cs
  31. 34 0
      src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs
  32. 17 8
      src/Avalonia.Base/ValueStore.cs
  33. 1 1
      src/Avalonia.Controls/Calendar/DatePicker.cs
  34. 2 1
      src/Avalonia.Controls/ComboBox.cs
  35. 195 0
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  36. 47 0
      src/Avalonia.Controls/MenuItem.cs
  37. 3 0
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  38. 0 11
      src/Avalonia.Controls/Primitives/AccessText.cs
  39. 1 1
      src/Avalonia.Controls/Primitives/Popup.cs
  40. 45 15
      src/Avalonia.Controls/TextBlock.cs
  41. 1 1
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  42. 1 1
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  43. 11 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  44. 36 8
      src/Avalonia.Input/KeyGesture.cs
  45. 3 3
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  46. 200 0
      src/Avalonia.Interactivity/EventRoute.cs
  47. 0 20
      src/Avalonia.Interactivity/EventSubscription.cs
  48. 10 3
      src/Avalonia.Interactivity/IInteractive.cs
  49. 96 197
      src/Avalonia.Interactivity/Interactive.cs
  50. 27 3
      src/Avalonia.Interactivity/InteractiveExtensions.cs
  51. 12 8
      src/Avalonia.Interactivity/RoutedEvent.cs
  52. 4 4
      src/Avalonia.Interactivity/RoutedEventArgs.cs
  53. 3 3
      src/Avalonia.Interactivity/RoutedEventRegistry.cs
  54. 0 1
      src/Avalonia.Styling/Avalonia.Styling.csproj
  55. 0 6
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  56. 65 52
      src/Avalonia.Styling/StyledElement.cs
  57. 0 77
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  58. 0 110
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  59. 0 133
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  60. 71 0
      src/Avalonia.Styling/Styling/Activators/AndActivator.cs
  61. 43 0
      src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs
  62. 33 0
      src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs
  63. 17 0
      src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs
  64. 16 0
      src/Avalonia.Styling/Styling/Activators/NotActivator.cs
  65. 71 0
      src/Avalonia.Styling/Styling/Activators/OrActivator.cs
  66. 45 0
      src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs
  67. 38 0
      src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
  68. 58 0
      src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs
  69. 76 0
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  70. 11 14
      src/Avalonia.Styling/Styling/DescendentSelector.cs
  71. 12 6
      src/Avalonia.Styling/Styling/ISetter.cs
  72. 40 0
      src/Avalonia.Styling/Styling/ISetterInstance.cs
  73. 6 10
      src/Avalonia.Styling/Styling/IStyle.cs
  74. 22 0
      src/Avalonia.Styling/Styling/IStyleInstance.cs
  75. 14 6
      src/Avalonia.Styling/Styling/IStyleable.cs
  76. 10 8
      src/Avalonia.Styling/Styling/NotSelector.cs
  77. 22 16
      src/Avalonia.Styling/Styling/OrSelector.cs
  78. 11 14
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  79. 180 0
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  80. 118 0
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  81. 81 48
      src/Avalonia.Styling/Styling/Selector.cs
  82. 47 11
      src/Avalonia.Styling/Styling/SelectorMatch.cs
  83. 59 87
      src/Avalonia.Styling/Styling/Setter.cs
  84. 27 141
      src/Avalonia.Styling/Styling/Style.cs
  85. 0 56
      src/Avalonia.Styling/Styling/StyleActivator.cs
  86. 135 0
      src/Avalonia.Styling/Styling/StyleInstance.cs
  87. 12 13
      src/Avalonia.Styling/Styling/Styler.cs
  88. 31 45
      src/Avalonia.Styling/Styling/Styles.cs
  89. 2 3
      src/Avalonia.Styling/Styling/TemplateSelector.cs
  90. 16 93
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  91. 27 8
      src/Avalonia.Themes.Default/MenuItem.xaml
  92. 1 0
      src/Avalonia.Themes.Default/NativeMenuBar.xaml
  93. 1 1
      src/Avalonia.Themes.Default/TabControl.xaml
  94. 27 0
      src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs
  95. 2 2
      src/Avalonia.Visuals/Media/Brush.cs
  96. 17 3
      src/Avalonia.Visuals/Media/Pen.cs
  97. 18 9
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  98. 11 5
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  99. 18 4
      src/Avalonia.Visuals/Visual.cs
  100. 2 17
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

+ 5 - 0
.ncrunch/BindingDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/RenderDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/VirtualizationDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 7 - 7
azure-pipelines.yml

@@ -14,7 +14,7 @@ jobs:
     displayName: 'Install Nuke'
     inputs:
       script: |
-         dotnet tool install --global Nuke.GlobalTool --version 0.12.3
+         dotnet tool install --global Nuke.GlobalTool --version 0.24.0
   - task: CmdLine@2
     displayName: 'Run Nuke'
     inputs:
@@ -35,16 +35,16 @@ jobs:
     vmImage: 'macOS-10.14'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.0.x'
+    displayName: 'Use .NET Core SDK 3.1.x'
     inputs:
       packageType: sdk
-      version: 3.0.x
+      version: 3.1.x
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core Runtime 2.1.x'
+    displayName: 'Use .NET Core Runtime 3.1.x'
     inputs:
       packageType: runtime
-      version: 2.1.x
+      version: 3.1.x
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'
@@ -74,7 +74,7 @@ jobs:
     displayName: 'Install Nuke'
     inputs:
       script: |
-       dotnet tool install --global Nuke.GlobalTool --version 0.12.3 
+       dotnet tool install --global Nuke.GlobalTool --version 0.24.0
 
   - task: CmdLine@2
     displayName: 'Run Nuke'
@@ -116,7 +116,7 @@ jobs:
     displayName: 'Install Nuke'
     inputs:
       script: |
-       dotnet tool install --global Nuke.GlobalTool --version 0.12.3 
+       dotnet tool install --global Nuke.GlobalTool --version 0.24.0 
 
   - task: CmdLine@2
     displayName: 'Run Nuke'

+ 3 - 0
global.json

@@ -1,4 +1,7 @@
 {
+	"sdk": {
+		"version": "3.1.101"
+	},
     "msbuild-sdks": {
         "Microsoft.Build.Traversal": "1.0.43",
         "MSBuild.Sdk.Extras": "2.0.46",

+ 80 - 86
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;
@@ -26,11 +27,13 @@ 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
 {
+    [Solution("Avalonia.sln")] readonly Solution Solution;
+
     static Lazy<string> MsBuildExe = new Lazy<string>(() =>
     {
         if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -54,7 +57,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,29 +96,24 @@ partial class Build : NukeBuild
         string projectFile,
         Configure<MSBuildSettings> configurator = null)
     {
-        return MSBuild(projectFile, c =>
-        {
+        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<string>("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(() =>
     {
-        DeleteDirectories(Parameters.BuildDirs);
-        EnsureCleanDirectories(Parameters.BuildDirs);
+        Parameters.BuildDirs.ForEach(DeleteDirectory);
+        Parameters.BuildDirs.ForEach(EnsureCleanDirectory);
         EnsureCleanDirectory(Parameters.ArtifactsDir);
         EnsureCleanDirectory(Parameters.NugetIntermediateRoot);
         EnsureCleanDirectory(Parameters.NugetRoot);
@@ -134,97 +132,84 @@ 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)
+
+    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<string> 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<string> {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) 
+                && 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);
-            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;
-            });
+            Information($"Running for {projectName} ({fw}) ...");
+
+            DotNetTest(c => c
+                .SetProjectFile(project)
+                .SetConfiguration(Parameters.Configuration)
+                .SetFramework(fw)
+                .EnableNoBuild()
+                .EnableNoRestore()
+                .When(Parameters.PublishTestResults, c => c
+                    .SetLogger("trx")
+                    .SetResultsDirectory(Parameters.TestResultsRoot)));
         }
     }
 
     Target RunCoreLibsTests => _ => _
-        .OnlyWhen(() => !Parameters.SkipTests)
+        .OnlyWhenStatic(() => !Parameters.SkipTests)
         .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 => _ => _
-        .OnlyWhen(() => !Parameters.SkipTests)
+        .OnlyWhenStatic(() => !Parameters.SkipTests)
         .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 => _ => _
-        .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
+        .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
         .DependsOn(Compile)
         .Executes(() =>
         {
-            RunCoreTest("./tests/Avalonia.DesignerSupport.Tests");
+            RunCoreTest("Avalonia.DesignerSupport.Tests");
         });
 
     [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 +220,7 @@ partial class Build : NukeBuild
         });
 
     Target ZipFiles => _ => _
-        .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)    
+        .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
         .Executes(() =>
         {
             var data = Parameters;
@@ -259,9 +244,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,32 +260,40 @@ 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<Build>(x => x.Package)
             : Execute<Build>(x => x.RunTests);
 
 }
+
+public static class ToolSettingsExtensions
+{
+    public static T Apply<T>(this T settings, Configure<T> configurator)
+    {
+        return configurator != null ? configurator(settings) : settings;
+    }
+}

+ 9 - 12
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,

+ 4 - 4
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<string> paths)
+    private void Zip(AbsolutePath target, IEnumerable<string> 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
             {

+ 4 - 4
nukebuild/_build.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <RootNamespace></RootNamespace>
     <IsPackable>False</IsPackable>
@@ -10,7 +10,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Nuke.Common" Version="0.12.3" />
+    <PackageReference Include="Nuke.Common" Version="0.24.0" />
     <PackageReference Include="xunit.runner.console" Version="2.3.1" />
     <PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" />
     <PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
@@ -20,11 +20,11 @@
     <NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
     <NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
     <None Remove="*.csproj.DotSettings;*.ref.*.txt" />
-    
+
     <!-- Common build related files -->
     <None Include="..\build.ps1" />
     <None Include="..\build.sh" />
-    <None Include="..\.nuke" />     
+    <None Include="..\.nuke" />
     <None Include="..\global.json" Condition="Exists('..\global.json')" />
     <None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
     <None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />

+ 3 - 3
packages/Avalonia/Avalonia.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-      <TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
+      <TargetFrameworks>netstandard2.0;net461;netcoreapp3.1</TargetFrameworks>
       <PackageId>Avalonia</PackageId>
   </PropertyGroup>
 
@@ -20,8 +20,8 @@
                          Platform=$(Platform)" />
 
     <ItemGroup>
-      <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll">
-        <PackagePath>tools/netcoreapp2.0/designer</PackagePath>
+      <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll">
+        <PackagePath>tools/netcoreapp3.1/designer</PackagePath>
         <Visible>false</Visible>
         <BuildAction>None</BuildAction>
       </_PackageFiles>

+ 1 - 1
packages/Avalonia/Avalonia.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
-    <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
+    <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
     <AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
     <AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
     <AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>

+ 1 - 1
samples/BindingDemo/BindingDemo.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

+ 1 - 1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
     <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
   </PropertyGroup>
 

+ 4 - 2
samples/ControlCatalog/MainWindow.xaml

@@ -14,7 +14,7 @@
       <NativeMenuItem Header="File">
         <NativeMenuItem.Menu>
           <NativeMenu>
-            <NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
+            <NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
             <NativeMenuItemSeperator/>
             <NativeMenuItem Header="Recent">
               <NativeMenuItem.Menu>
@@ -22,7 +22,9 @@
               </NativeMenuItem.Menu>
             </NativeMenuItem>
             <NativeMenuItemSeperator/>
-            <NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/>
+            <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
+                            Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
+                            Clicked="OnCloseClicked" />
           </NativeMenu>
         </NativeMenuItem.Menu>
       </NativeMenuItem>

+ 9 - 5
samples/ControlCatalog/MainWindow.xaml.cs

@@ -1,13 +1,11 @@
+using System;
+using System.Runtime.InteropServices;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Notifications;
-using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Markup.Xaml;
-using Avalonia.Threading;
 using ControlCatalog.ViewModels;
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
 
 namespace ControlCatalog
 {
@@ -35,6 +33,12 @@ namespace ControlCatalog
             mainMenu.AttachedToVisualTree += MenuAttached;
         }
 
+        public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
+
+        public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
+            new KeyGesture(Key.Q, KeyModifiers.Meta) :
+            new KeyGesture(Key.F4, KeyModifiers.Alt);
+
         public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
         {
             if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)

+ 2 - 2
samples/ControlCatalog/Pages/MenuPage.xaml

@@ -16,13 +16,13 @@
                 <TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
                 <Menu>
                     <MenuItem Header="_First">
-                        <MenuItem Header="Standard _Menu Item"/>
+                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
                         <Separator/>
                         <MenuItem Header="Menu with _Submenu">
                             <MenuItem Header="Submenu _1"/>
                             <MenuItem Header="Submenu _2"/>
                         </MenuItem>
-                        <MenuItem Header="Menu Item with _Icon">
+                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
                             <MenuItem.Icon>
                                 <Image Source="/Assets/github_icon.png"/>
                             </MenuItem.Icon>

+ 1 - 1
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 1 - 1
samples/Previewer/Previewer.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
   <ItemGroup>
     <Compile Update="**\*.xaml.cs">

+ 1 - 1
samples/RemoteDemo/RemoteDemo.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

+ 1 - 1
samples/RenderDemo/RenderDemo.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

+ 1 - 1
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

+ 7 - 2
src/Avalonia.Base/AvaloniaObject.cs

@@ -311,7 +311,10 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
-        public void SetValue<T>(
+        /// <returns>
+        /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
+        /// </returns>
+        public IDisposable SetValue<T>(
             StyledPropertyBase<T> 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;
         }
 
         /// <summary>

+ 11 - 6
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -458,7 +458,10 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
-        public static void SetValue(
+        /// <returns>
+        /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
+        /// </returns>
+        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);
         }
 
         /// <summary>
@@ -478,7 +481,10 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
-        public static void SetValue<T>(
+        /// <returns>
+        /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
+        /// </returns>
+        public static IDisposable SetValue<T>(
             this IAvaloniaObject target,
             AvaloniaProperty<T> property,
             T value,
@@ -490,11 +496,10 @@ namespace Avalonia
             switch (property)
             {
                 case StyledPropertyBase<T> styled:
-                    target.SetValue(styled, value, priority);
-                    break;
+                    return target.SetValue(styled, value, priority);
                 case DirectPropertyBase<T> direct:
                     target.SetValue(direct, value);
-                    break;
+                    return null;
                 default:
                     throw new NotSupportedException("Unsupported AvaloniaProperty type.");
             }

+ 13 - 1
src/Avalonia.Base/AvaloniaProperty.cs

@@ -469,6 +469,15 @@ namespace Avalonia
             return Name;
         }
 
+        /// <summary>
+        /// Uses the visitor pattern to resolve an untyped property to a typed property.
+        /// </summary>
+        /// <typeparam name="TData">The type of user data passed.</typeparam>
+        /// <param name="vistor">The visitor which will accept the typed property.</param>
+        /// <param name="data">The user data to pass.</param>
+        public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
+            where TData : struct;
+
         /// <summary>
         /// Notifies the <see cref="Changed"/> observable.
         /// </summary>
@@ -496,7 +505,10 @@ namespace Avalonia
         /// <param name="o">The object instance.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority.</param>
-        internal abstract void RouteSetValue(
+        /// <returns>
+        /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
+        /// </returns>
+        internal abstract IDisposable? RouteSetValue(
             IAvaloniaObject o,
             object value,
             BindingPriority priority);

+ 10 - 1
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<TValue>)base.GetMetadata(type);
         }
 
+        /// <inheritdoc/>
+        public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
+        {
+            vistor.Visit(this, ref data);
+        }
+
         /// <inheritdoc/>
         internal override void RouteClearValue(IAvaloniaObject o)
         {
@@ -114,7 +121,7 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        internal override void RouteSetValue(
+        internal override IDisposable? RouteSetValue(
             IAvaloniaObject o,
             object value,
             BindingPriority priority)
@@ -133,6 +140,8 @@ namespace Avalonia
             {
                 throw v.Error!;
             }
+
+            return null;
         }
 
         /// <inheritdoc/>

+ 1 - 1
src/Avalonia.Base/IAvaloniaObject.cs

@@ -65,7 +65,7 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <param name="value">The value.</param>
         /// <param name="priority">The priority of the value.</param>
-        void SetValue<T>(
+        IDisposable SetValue<T>(
             StyledPropertyBase<T> property,
             T value,
             BindingPriority priority = BindingPriority.LocalValue);

+ 8 - 3
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore
     /// <see cref="PriorityValue{T}"/>.
     /// </summary>
     /// <typeparam name="T">The property type.</typeparam>
-    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>
+    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
     {
+        private IValueSink _sink;
+
         public ConstantValueEntry(
             StyledPropertyBase<T> property,
             T value,
-            BindingPriority priority)
+            BindingPriority priority,
+            IValueSink sink)
         {
             Property = property;
             Value = value;
             Priority = priority;
+            _sink = sink;
         }
 
         public StyledPropertyBase<T> Property { get; }
@@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore
         Optional<object> 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;
     }
 }

+ 7 - 2
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<T>(Property, value, priority));
+                var entry = new ConstantValueEntry<T>(Property, value, priority, this);
+                _entries.Insert(insert, entry);
+                result = entry;
             }
 
             UpdateEffectiveValue();
+            return result;
         }
 
         public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)

+ 1 - 1
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@@ -36,7 +36,7 @@ namespace Avalonia.Reactive
             return this;
         }
 
-        void IDisposable.Dispose()
+        public virtual void Dispose()
         {
             Unsubscribed();
             _observer = null;

+ 11 - 2
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);
         }
 
+        /// <inheritdoc/>
+        public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
+        {
+            vistor.Visit(this, ref data);
+        }
+
         /// <summary>
         /// Gets the string representation of the property.
         /// </summary>
@@ -194,7 +201,7 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        internal override void RouteSetValue(
+        internal override IDisposable RouteSetValue(
             IAvaloniaObject o,
             object value,
             BindingPriority priority)
@@ -203,7 +210,7 @@ namespace Avalonia
 
             if (v.HasValue)
             {
-                o.SetValue<TValue>(this, (TValue)v.Value, priority);
+                return o.SetValue<TValue>(this, (TValue)v.Value, priority);
             }
             else if (v.Type == BindingValueType.UnsetValue)
             {
@@ -213,6 +220,8 @@ namespace Avalonia
             {
                 throw v.Error;
             }
+
+            return null;
         }
 
         /// <inheritdoc/>

+ 34 - 0
src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs

@@ -0,0 +1,34 @@
+#nullable enable
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
+    /// </summary>
+    /// <typeparam name="TData">The type of user data passed.</typeparam>
+    /// <remarks>
+    /// Pass an instance that implements this interface to
+    /// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
+    /// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
+    /// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
+    /// </remarks>
+    public interface IAvaloniaPropertyVisitor<TData>
+        where TData : struct
+    {
+        /// <summary>
+        /// Called when the property is a styled property.
+        /// </summary>
+        /// <typeparam name="T">The property value type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="data">The user data.</param>
+        void Visit<T>(StyledPropertyBase<T> property, ref TData data);
+
+        /// <summary>
+        /// Called when the property is a direct property.
+        /// </summary>
+        /// <typeparam name="T">The property value type.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="data">The user data.</param>
+        void Visit<T>(DirectPropertyBase<T> property, ref TData data);
+    }
+}

+ 17 - 8
src/Avalonia.Base/ValueStore.cs

@@ -70,23 +70,25 @@ namespace Avalonia
             return false;
         }
 
-        public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
+        public IDisposable? SetValue<T>(StyledPropertyBase<T> 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<T>(_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<T>(property, value, priority);
+                var entry = new ConstantValueEntry<T>(property, value, priority, this);
                 _values.AddValue(property, entry);
                 _sink.ValueChanged(property, priority, default, value);
+                result = entry;
             }
+
+            return result;
         }
 
         public IDisposable AddBinding<T>(
@@ -205,21 +210,23 @@ namespace Avalonia
             }
         }
 
-        private void SetExisting<T>(
+        private IDisposable? SetExisting<T>(
             object slot,
             StyledPropertyBase<T> property,
             T value,
             BindingPriority priority)
         {
+            IDisposable? result = null;
+
             if (slot is IPriorityValueEntry<T> e)
             {
                 var priorityValue = new PriorityValue<T>(_owner, property, this, e);
                 _values.SetValue(property, priorityValue);
-                priorityValue.SetValue(value, priority);
+                result = priorityValue.SetValue(value, priority);
             }
             else if (slot is PriorityValue<T> p)
             {
-                p.SetValue(value, priority);
+                result = p.SetValue(value, priority);
             }
             else if (slot is LocalValueEntry<T> l)
             {
@@ -232,7 +239,7 @@ namespace Avalonia
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_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<T>(

+ 1 - 1
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -476,7 +476,7 @@ namespace Avalonia.Controls
             {
                 _dropDownButton.Click += DropDownButton_Click;
                 _buttonPointerPressedSubscription =
-                    _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
+                    _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
             }
 
             if (_textBox != null)

+ 2 - 1
src/Avalonia.Controls/ComboBox.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.VisualTree;
@@ -265,7 +266,7 @@ namespace Avalonia.Controls
             var toplevel = this.GetVisualRoot() as TopLevel;
             if (toplevel != null)
             {
-                _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
+                _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
                 {
                     //eat wheel scroll event outside dropdown popup while it's open
                     if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)

+ 195 - 0
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@@ -0,0 +1,195 @@
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Data.Converters;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Converters
+{
+    /// <summary>
+    /// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
+    /// platform's style guidelines.
+    /// </summary>
+    public class PlatformKeyGestureConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is null)
+            {
+                return null;
+            }
+            else if (value is KeyGesture gesture && targetType == typeof(string))
+            {
+                return ToPlatformString(gesture);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
+        /// platform's style guidelines.
+        /// </summary>
+        /// <param name="gesture">The gesture.</param>
+        /// <returns>The gesture formatted according to the current platform.</returns>
+        public static string ToPlatformString(KeyGesture gesture)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return ToString(gesture, "Win");
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                return ToString(gesture, "Super");
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                return ToOSXString(gesture);
+            }
+            else
+            {
+                return gesture.ToString();
+            }
+        }
+
+        private static string ToString(KeyGesture gesture, string meta)
+        {
+            var s = new StringBuilder();
+
+            static void Plus(StringBuilder s)
+            {
+                if (s.Length > 0)
+                {
+                    s.Append("+");
+                }
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append("Ctrl");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                Plus(s);
+                s.Append("Shift");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                Plus(s);
+                s.Append("Alt");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                Plus(s);
+                s.Append(meta);
+            }
+
+            Plus(s);
+            s.Append(ToString(gesture.Key));
+
+            return s.ToString();
+        }
+
+        private static string ToOSXString(KeyGesture gesture)
+        {
+            var s = new StringBuilder();
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append('⌃');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                s.Append('⌥');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                s.Append('⇧');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                s.Append('⌘');
+            }
+
+            s.Append(ToOSXString(gesture.Key));
+
+            return s.ToString();
+        }
+
+        private static string ToString(Key key)
+        {
+            return key switch
+            {
+                Key.Add => "+",
+                Key.Back => "Backspace",
+                Key.D0 => "0",
+                Key.D1 => "1",
+                Key.D2 => "2",
+                Key.D3 => "3",
+                Key.D4 => "4",
+                Key.D5 => "5",
+                Key.D6 => "6",
+                Key.D7 => "7",
+                Key.D8 => "8",
+                Key.D9 => "9",
+                Key.Decimal => ".",
+                Key.Divide => "/",
+                Key.Down => "Down Arrow",
+                Key.Left => "Left Arrow",
+                Key.Multiply => "*",
+                Key.OemBackslash => "\\",
+                Key.OemCloseBrackets => "]",
+                Key.OemComma => ",",
+                Key.OemMinus => "-",
+                Key.OemOpenBrackets => "[",
+                Key.OemPeriod=> ".",
+                Key.OemPipe => "|",
+                Key.OemPlus => "+",
+                Key.OemQuestion => "/",
+                Key.OemQuotes => "\"",
+                Key.OemSemicolon => ";",
+                Key.OemTilde => "`",
+                Key.Right => "Right Arrow",
+                Key.Separator => "/",
+                Key.Subtract => "-",
+                Key.Up => "Up Arrow",
+                _ => key.ToString(),
+            };
+        }
+
+        private static string ToOSXString(Key key)
+        {
+            return key switch
+            {
+                Key.Back => "⌫",
+                Key.Down => "↓",
+                Key.End => "↘",
+                Key.Escape => "⎋",
+                Key.Home => "↖",
+                Key.Left => "←",
+                Key.Return => "↩",
+                Key.PageDown => "⇞",
+                Key.PageUp => "⇟",
+                Key.Right => "→",
+                Key.Space => "␣",
+                Key.Tab => "⇥",
+                Key.Up => "↑",
+                _ => ToString(key),
+            };
+        }
+    }
+}

+ 47 - 0
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<object> IconProperty =
             AvaloniaProperty.Register<MenuItem, object>(nameof(Icon));
 
+        /// <summary>
+        /// Defines the <see cref="InputGesture"/> property.
+        /// </summary>
+        public static readonly StyledProperty<KeyGesture> InputGestureProperty =
+            AvaloniaProperty.Register<MenuItem, KeyGesture>(nameof(InputGesture));
+
         /// <summary>
         /// Defines the <see cref="IsSelected"/> property.
         /// </summary>
@@ -93,6 +100,7 @@ namespace Avalonia.Controls
         private ICommand _command;
         private bool _commandCanExecute = true;
         private Popup _popup;
+        private IDisposable _gridHack;
 
         /// <summary>
         /// Initializes static members of the <see cref="MenuItem"/> class.
@@ -194,6 +202,19 @@ namespace Avalonia.Controls
             set { SetValue(IconProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the input gesture that will be displayed in the menu item.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        public KeyGesture InputGesture
+        {
+            get { return GetValue(InputGestureProperty); }
+            set { SetValue(InputGestureProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
         /// </summary>
@@ -304,6 +325,32 @@ namespace Avalonia.Controls
             {
                 Command.CanExecuteChanged -= CanExecuteChanged;
             }
+
+            _gridHack?.Dispose();
+            _gridHack = null;
+        }
+
+        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));
+            }
         }
 
         /// <summary>

+ 3 - 0
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -82,6 +82,7 @@ namespace Avalonia.Controls.Presenters
                 SelectionStartProperty, SelectionEndProperty);
 
             Observable.Merge(
+                TextProperty.Changed,
                 SelectionStartProperty.Changed,
                 SelectionEndProperty.Changed,
                 PasswordCharProperty.Changed
@@ -291,6 +292,8 @@ namespace Avalonia.Controls.Presenters
                 _constraint = _formattedText.Constraint;
                 _formattedText = null;
             }
+
+            InvalidateVisual();
         }
 
         /// <summary>

+ 0 - 11
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives
             return base.CreateTextLayout(constraint, StripAccessKey(text));
         }
 
-        /// <summary>
-        /// Measures the control.
-        /// </summary>
-        /// <param name="availableSize">The available size for the control.</param>
-        /// <returns>The desired size.</returns>
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            var result = base.MeasureOverride(availableSize);
-            return result.WithHeight(result.Height + 1);
-        }
-
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {

+ 1 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
+            DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
 
             DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
 

+ 45 - 15
src/Avalonia.Controls/TextBlock.cs

@@ -20,6 +20,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IBrush> BackgroundProperty =
             Border.BackgroundProperty.AddOwner<TextBlock>();
 
+        /// <summary>
+        /// Defines the <see cref="Padding"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Thickness> PaddingProperty =
+            Decorator.PaddingProperty.AddOwner<TextBlock>();
+
         // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner
         // them into TextBlock.
 
@@ -29,7 +35,7 @@ namespace Avalonia.Controls
         public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
             AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>(
                 nameof(FontFamily),
-                defaultValue:  FontFamily.Default,
+                defaultValue: FontFamily.Default,
                 inherits: true);
 
         /// <summary>
@@ -110,20 +116,31 @@ namespace Avalonia.Controls
         static TextBlock()
         {
             ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
+
             AffectsRender<TextBlock>(
-                BackgroundProperty,
-                ForegroundProperty,
-                FontWeightProperty,
-                FontSizeProperty,
-                FontStyleProperty);
+                BackgroundProperty, ForegroundProperty, FontSizeProperty, 
+                FontWeightProperty, FontStyleProperty, TextWrappingProperty, 
+                TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, 
+                TextDecorationsProperty, TextProperty, PaddingProperty);
+
+            AffectsMeasure<TextBlock>(
+                FontSizeProperty, FontWeightProperty, FontStyleProperty, 
+                FontFamilyProperty, TextTrimmingProperty, TextProperty,
+                PaddingProperty);
 
             Observable.Merge(
                 TextProperty.Changed,
+                ForegroundProperty.Changed,
                 TextAlignmentProperty.Changed,
+                TextWrappingProperty.Changed,
+                TextTrimmingProperty.Changed,
                 FontSizeProperty.Changed,
                 FontStyleProperty.Changed,
-                FontWeightProperty.Changed
-            ).AddClassHandler<TextBlock>((x, _) => x.OnTextPropertiesChanged());
+                FontWeightProperty.Changed,
+                FontFamilyProperty.Changed,
+                TextDecorationsProperty.Changed,
+                PaddingProperty.Changed
+            ).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
         }
 
         /// <summary>
@@ -145,6 +162,15 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Gets or sets the padding to place around the <see cref="Text"/>.
+        /// </summary>
+        public Thickness Padding
+        {
+            get { return GetValue(PaddingProperty); }
+            set { SetValue(PaddingProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a brush used to paint the control's background.
         /// </summary>
@@ -363,7 +389,9 @@ namespace Avalonia.Controls
                 context.FillRectangle(background, new Rect(Bounds.Size));
             }
 
-            TextLayout?.Draw(context.PlatformImpl, new Point());
+            var padding = Padding;
+
+            TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
         }
 
         /// <summary>
@@ -412,6 +440,10 @@ namespace Avalonia.Controls
                 return new Size();
             }
 
+            var padding = Padding;
+
+            availableSize = availableSize.Deflate(padding);
+
             if (_constraint != availableSize)
             {
                 InvalidateTextLayout();
@@ -419,19 +451,17 @@ namespace Avalonia.Controls
 
             _constraint = availableSize;
 
-            return TextLayout?.Bounds.Size ?? Size.Empty;
+            var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
+
+            return measuredSize.Inflate(padding);
         }
 
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            InvalidateTextLayout();
-            InvalidateMeasure();
-        }
 
-        private void OnTextPropertiesChanged()
-        {
             InvalidateTextLayout();
+
             InvalidateMeasure();
         }
     }

+ 1 - 1
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
+    <TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
   </PropertyGroup>
 
   <ItemGroup>

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics
                 }
             }
 
-            return root.AddHandler(
+            return root.AddDisposableHandler(
                 InputElement.KeyDownEvent,
                 PreviewKeyDown,
                 RoutingStrategies.Tunnel);

+ 11 - 6
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<LogicalTreeAttachmentEventArgs>(
+                    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
                         {

+ 36 - 8
src/Avalonia.Input/KeyGesture.cs

@@ -1,9 +1,11 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
+// 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.Runtime.InteropServices;
+using System.Text;
 
 namespace Avalonia.Input
 {
@@ -108,19 +110,43 @@ namespace Avalonia.Input
 
         public override string ToString()
         {
-            var parts = new List<string>();
+            var s = new StringBuilder();
 
-            foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast<KeyModifiers>())
+            static void Plus(StringBuilder s)
             {
-                if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None)
+                if (s.Length > 0)
                 {
-                    parts.Add(flag.ToString());
+                    s.Append("+");
                 }
             }
 
-            parts.Add(Key.ToString());
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append("Ctrl");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                Plus(s);
+                s.Append("Shift");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                Plus(s);
+                s.Append("Alt");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                Plus(s);
+                s.Append("Cmd");
+            }
+
+            Plus(s);
+            s.Append(Key);
 
-            return string.Join(" + ", parts);
+            return s.ToString();
         }
 
         public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers;
@@ -141,7 +167,9 @@ namespace Avalonia.Input
                 return KeyModifiers.Control;
             }
 
-            if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase))
+            if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+                modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+                modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase))
             {
                 return KeyModifiers.Meta;
             }

+ 3 - 3
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@@ -1,6 +1,8 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <Nullable>Enable</Nullable>
+    <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@@ -9,6 +11,4 @@
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\Rx.props" />
-</Project>
-
-
+</Project>

+ 200 - 0
src/Avalonia.Interactivity/EventRoute.cs

@@ -0,0 +1,200 @@
+using System;
+using Avalonia.Collections.Pooled;
+
+namespace Avalonia.Interactivity
+{
+    /// <summary>
+    /// Holds the route for a routed event and supports raising an event on that route.
+    /// </summary>
+    public class EventRoute : IDisposable
+    {
+        private readonly RoutedEvent _event;
+        private PooledList<RouteItem>? _route;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RoutedEvent"/> class.
+        /// </summary>
+        /// <param name="e">The routed event to be raised.</param>
+        public EventRoute(RoutedEvent e)
+        {
+            e = e ?? throw new ArgumentNullException(nameof(e));
+
+            _event = e;
+            _route = null;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the route has any handlers.
+        /// </summary>
+        public bool HasHandlers => _route?.Count > 0;
+
+        /// <summary>
+        /// Adds a handler to the route.
+        /// </summary>
+        /// <param name="target">The target on which the event should be raised.</param>
+        /// <param name="handler">The handler for the event.</param>
+        /// <param name="routes">The routing strategies to listen to.</param>
+        /// <param name="handledEventsToo">
+        /// If true the handler will be raised even when the routed event is marked as handled.
+        /// </param>
+        /// <param name="adapter">
+        /// An optional adapter which if supplied, will be called with <paramref name="handler"/>
+        /// and the parameters for the event. This adapter can be used to avoid calling
+        /// `DynamicInvoke` on the handler.
+        /// </param>
+        public void Add(
+            IInteractive target,
+            Delegate handler,
+            RoutingStrategies routes,
+            bool handledEventsToo = false,
+            Action<Delegate, object, RoutedEventArgs>? adapter = null)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
+
+            _route ??= new PooledList<RouteItem>(16);
+            _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo));
+        }
+
+        /// <summary>
+        /// Adds a class handler to the route.
+        /// </summary>
+        /// <param name="target">The target on which the event should be raised.</param>
+        public void AddClassHandler(IInteractive target)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+
+            _route ??= new PooledList<RouteItem>(16);
+            _route.Add(new RouteItem(target, null, null, 0, false));
+        }
+
+        /// <summary>
+        /// Raises an event along the route.
+        /// </summary>
+        /// <param name="source">The event source.</param>
+        /// <param name="e">The event args.</param>
+        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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Disposes of the event route.
+        /// </summary>
+        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<Delegate, object, RoutedEventArgs>? 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<Delegate, object, RoutedEventArgs>? Adapter { get; }
+            public RoutingStrategies Routes { get; }
+            public bool HandledEventsToo { get; }
+        }
+    }
+}

+ 0 - 20
src/Avalonia.Interactivity/EventSubscription.cs

@@ -1,20 +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 delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
-
-    internal class EventSubscription
-    {
-        public HandlerInvokeSignature InvokeAdapter { get; set; }
-
-        public Delegate Handler { get; set; }
-
-        public RoutingStrategies Routes { get; set; }
-
-        public bool AlsoIfHandled { get; set; }
-    }
-}

+ 10 - 3
src/Avalonia.Interactivity/IInteractive.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Interactivity
         /// <summary>
         /// Gets the interactive parent of the object for bubbling and tunneling events.
         /// </summary>
-        IInteractive InteractiveParent { get; }
+        IInteractive? InteractiveParent { get; }
 
         /// <summary>
         /// Adds a handler for the specified routed event.
@@ -23,7 +23,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler(
+        void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -38,7 +38,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler<TEventArgs>(
+        void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -60,6 +60,13 @@ namespace Avalonia.Interactivity
         void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
             where TEventArgs : RoutedEventArgs;
 
+        /// <summary>
+        /// Adds the object's handlers for a routed event to an event route.
+        /// </summary>
+        /// <param name="routedEvent">The event.</param>
+        /// <param name="route">The event route.</param>
+        void AddToEventRoute(RoutedEvent routedEvent, EventRoute route);
+
         /// <summary>
         /// Raises a routed event.
         /// </summary>

+ 96 - 197
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;
 
@@ -15,16 +13,12 @@ namespace Avalonia.Interactivity
     /// </summary>
     public class Interactive : Layoutable, IInteractive
     {
-        private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
-
-        private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
+        private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
 
         /// <summary>
         /// Gets the interactive parent of the object for bubbling and tunneling events.
         /// </summary>
-        IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
-
-        private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
+        IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
 
         /// <summary>
         /// Adds a handler for the specified routed event.
@@ -33,24 +27,18 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler(
+        public void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
             bool handledEventsToo = false)
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(handler != null);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
-            var subscription = new EventSubscription
-            {
-                Handler = handler,
-                Routes = routes,
-                AlsoIfHandled = handledEventsToo,
-            };
+            var subscription = new EventSubscription(handler, routes, handledEventsToo);
 
-            return AddEventSubscription(routedEvent, subscription);
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -61,44 +49,26 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler<TEventArgs>(
+        public void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
             bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(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;
-
-            if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter))
+            static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
             {
-                void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
-                {
-                    var typedHandler = (EventHandler<TEventArgs>)baseHandler;
-                    var typedArgs = (TEventArgs)args;
+                var typedHandler = (EventHandler<TEventArgs>)baseHandler;
+                var typedArgs = (TEventArgs)args;
 
-                    typedHandler(sender, typedArgs);
-                }
-
-                invokeAdapter = InvokeAdapter;
-
-                s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
+                typedHandler(sender, typedArgs);
             }
 
-            var subscription = new EventSubscription
-            {
-                InvokeAdapter = invokeAdapter,
-                Handler = handler,
-                Routes = routes,
-                AlsoIfHandled = handledEventsToo,
-            };
+            var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
 
-            return AddEventSubscription(routedEvent, subscription);
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -108,14 +78,19 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(handler != null);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
-            List<EventSubscription> subscriptions = null;
-
-            if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true)
+            if (_eventHandlers is object &&
+                _eventHandlers.TryGetValue(routedEvent, out var subscriptions))
             {
-                subscriptions.RemoveAll(x => x.Handler == handler);
+                for (var i = subscriptions.Count - 1; i >= 0; i--)
+                {
+                    if (subscriptions[i].Handler == handler)
+                    {
+                        subscriptions.RemoveAt(i);
+                    }
+                }
             }
         }
 
@@ -137,191 +112,115 @@ namespace Avalonia.Interactivity
         /// <param name="e">The event args.</param>
         public void RaiseEvent(RoutedEventArgs e)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Source = e.Source ?? this;
-
-            if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct)
-            {
-                e.Route = RoutingStrategies.Direct;
-                RaiseEventImpl(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
-            }
+            e = e ?? throw new ArgumentNullException(nameof(e));
 
-            if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
+            if (e.RoutedEvent == null)
             {
-                TunnelEvent(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
+                throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
             }
 
-            if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0)
-            {
-                BubbleEvent(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
-            }
+            using var route = BuildEventRoute(e.RoutedEvent);
+            route.RaiseEvent(this, e);
         }
 
-        /// <summary>
-        /// Bubbles an event.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        private void BubbleEvent(RoutedEventArgs e)
-        {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Route = RoutingStrategies.Bubble;
-
-            var traverser = HierarchyTraverser<RaiseEventTraverse, NopTraverse>.Create(e);
-
-            traverser.Traverse(this);
-        }
-
-        /// <summary>
-        /// Tunnels an event.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        private void TunnelEvent(RoutedEventArgs e)
+        void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Route = RoutingStrategies.Tunnel;
-
-            var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.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);
+                }
+            }
         }
 
         /// <summary>
-        /// Carries out the actual invocation of an event on this object.
+        /// Builds an event route for a routed event.
         /// </summary>
-        /// <param name="e">The event args.</param>
-        private void RaiseEventImpl(RoutedEventArgs e)
+        /// <param name="e">The routed event.</param>
+        /// <returns>An <see cref="EventRoute"/> describing the route.</returns>
+        /// <remarks>
+        /// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> 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 <see cref="EventRoute.HasHandlers"/>
+        /// property to see if there are any handlers registered on the route. If there are, call
+        /// <see cref="EventRoute.RaiseEvent(IInteractive, RoutedEventArgs)"/> to raise the event.
+        /// </remarks>
+        protected EventRoute BuildEventRoute(RoutedEvent e)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
+            e = e ?? throw new ArgumentNullException(nameof(e));
 
-            e.RoutedEvent.InvokeRaised(this, e);
+            var result = new EventRoute(e);
+            var hasClassHandlers = e.HasRaisedSubscriptions;
 
-            List<EventSubscription> subscriptions = null;
-
-            if (_eventHandlers?.TryGetValue(e.RoutedEvent, out 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.AlsoIfHandled;
+                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<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
-        {
-            if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
+            else
             {
-                subscriptions = new List<EventSubscription>();
-                EventHandlers.Add(routedEvent, subscriptions);
-            }
-
-            return subscriptions;
-        }
-
-        private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
-        {
-            List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
-
-            subscriptions.Add(subscription);
-
-            return new UnsubscribeDisposable(subscriptions, subscription);
-        }
-
-        private sealed class UnsubscribeDisposable : IDisposable
-        {
-            private readonly List<EventSubscription> _subscriptions;
-            private readonly EventSubscription _subscription;
+                if (hasClassHandlers)
+                {
+                    result.AddClassHandler(this);
+                }
 
-            public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
-            {
-                _subscriptions = subscriptions;
-                _subscription = subscription;
+                ((IInteractive)this).AddToEventRoute(e, result);
             }
 
-            public void Dispose()
-            {
-                _subscriptions.Remove(_subscription);
-            }
+            return result;
         }
 
-        private interface ITraverse
+        private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
         {
-            void Execute(IInteractive target, RoutedEventArgs e);
-        }
+            _eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
 
-        private struct NopTraverse : ITraverse
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            public void Execute(IInteractive target, RoutedEventArgs e)
+            if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
             {
+                subscriptions = new List<EventSubscription>();
+                _eventHandlers.Add(routedEvent, subscriptions);
             }
-        }
 
-        private struct RaiseEventTraverse : ITraverse
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            public void Execute(IInteractive target, RoutedEventArgs e)
-            {
-                ((Interactive)target).RaiseEventImpl(e);
-            }
+            subscriptions.Add(subscription);
         }
 
-        /// <summary>
-        /// Traverses interactive hierarchy allowing for raising events.
-        /// </summary>
-        /// <typeparam name="TPreTraverse">Called before parent is traversed.</typeparam>
-        /// <typeparam name="TPostTraverse">Called after parent has been traversed.</typeparam>
-        private struct HierarchyTraverser<TPreTraverse, TPostTraverse>
-            where TPreTraverse : struct, ITraverse
-            where TPostTraverse : struct, ITraverse
+        private readonly struct EventSubscription
         {
-            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<TPreTraverse, TPostTraverse> Create(RoutedEventArgs args)
-            {
-                return new HierarchyTraverser<TPreTraverse, TPostTraverse>(new TPreTraverse(), new TPostTraverse(), args);
+            public EventSubscription(
+                Delegate handler,
+                RoutingStrategies routes,
+                bool handledEventsToo,
+                Action<Delegate, object, RoutedEventArgs>? invokeAdapter = null)
+            {
+                Handler = handler;
+                Routes = routes;
+                HandledEventsToo = handledEventsToo;
+                InvokeAdapter = invokeAdapter;
             }
 
-            public void Traverse(IInteractive target)
-            {
-                _preTraverse.Execute(target, _args);
+            public Action<Delegate, object, RoutedEventArgs>? InvokeAdapter { get; }
 
-                IInteractive parent = target.InteractiveParent;
+            public Delegate Handler { get; }
 
-                if (parent != null)
-                {
-                    Traverse(parent);
-                }
+            public RoutingStrategies Routes { get; }
 
-                _postTraverse.Execute(target, _args);
-            }
+            public bool HandledEventsToo { get; }
         }
     }
 }

+ 27 - 3
src/Avalonia.Interactivity/InteractiveExtensions.cs

@@ -2,8 +2,7 @@
 // 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.Disposables;
 using System.Reactive.Linq;
 
 namespace Avalonia.Interactivity
@@ -13,6 +12,28 @@ namespace Avalonia.Interactivity
     /// </summary>
     public static class InteractiveExtensions
     {
+        /// <summary>
+        /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription.
+        /// </summary>
+        /// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
+        /// <param name="o">Target for adding given event handler.</param>
+        /// <param name="routedEvent">The routed event.</param>
+        /// <param name="handler">The handler.</param>
+        /// <param name="routes">The routing strategies to listen to.</param>
+        /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
+        /// <returns>A disposable that terminates the event subscription.</returns>
+        public static IDisposable AddDisposableHandler<TEventArgs>(this IInteractive o, RoutedEvent<TEventArgs> routedEvent,
+            EventHandler<TEventArgs> handler,
+            RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
+            bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
+        {
+            o.AddHandler(routedEvent, handler, routes, handledEventsToo);
+
+            return Disposable.Create(
+                (instance: o, handler, routedEvent),
+                state => state.instance.RemoveHandler(state.routedEvent, state.handler));
+        }
+
         /// <summary>
         /// Gets an observable for a <see cref="RoutedEvent{TEventArgs}"/>.
         /// </summary>
@@ -30,7 +51,10 @@ namespace Avalonia.Interactivity
             bool handledEventsToo = false)
                 where TEventArgs : RoutedEventArgs
         {
-            return Observable.Create<TEventArgs>(x => o.AddHandler(
+            o = o ?? throw new ArgumentNullException(nameof(o));
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+
+            return Observable.Create<TEventArgs>(x => o.AddDisposableHandler(
                 routedEvent, 
                 (_, e) => x.OnNext(e), 
                 routes,

+ 12 - 8
src/Avalonia.Interactivity/RoutedEvent.cs

@@ -25,10 +25,14 @@ namespace Avalonia.Interactivity
             Type eventArgsType,
             Type ownerType)
         {
-            Contract.Requires<ArgumentNullException>(name != null);
-            Contract.Requires<ArgumentNullException>(eventArgsType != null);
-            Contract.Requires<ArgumentNullException>(ownerType != null);
-            Contract.Requires<InvalidCastException>(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;
@@ -44,6 +48,8 @@ namespace Avalonia.Interactivity
 
         public RoutingStrategies RoutingStrategies { get; }
 
+        public bool HasRaisedSubscriptions => _raised.HasObservers;
+
         public IObservable<(object, RoutedEventArgs)> Raised => _raised;
         public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
 
@@ -52,7 +58,7 @@ namespace Avalonia.Interactivity
             RoutingStrategies routingStrategy)
                 where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(name != null);
+            name = name ?? throw new ArgumentNullException(nameof(name));
 
             var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
             RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent);
@@ -65,7 +71,7 @@ namespace Avalonia.Interactivity
             Type ownerType)
                 where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(name != null);
+            name = name ?? throw new ArgumentNullException(nameof(name));
 
             var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
             RoutedEventRegistry.Instance.Register(ownerType, routedEvent);
@@ -108,8 +114,6 @@ namespace Avalonia.Interactivity
         public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
             : base(name, routingStrategies, typeof(TEventArgs), ownerType)
         {
-            Contract.Requires<ArgumentNullException>(name != null);
-            Contract.Requires<ArgumentNullException>(ownerType != null);
         }
 
         [Obsolete("Use overload taking Action<TTarget, TEventArgs>.")]

+ 4 - 4
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; }
     }
 }

+ 3 - 3
src/Avalonia.Interactivity/RoutedEventRegistry.cs

@@ -32,8 +32,8 @@ namespace Avalonia.Interactivity
         /// </remarks>
         public void Register(Type type, RoutedEvent @event)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
-            Contract.Requires<ArgumentNullException>(@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
         /// <returns>All routed events registered with the provided type.</returns>
         public IReadOnlyList<RoutedEvent> GetRegistered(Type type)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
+            type = type ?? throw new ArgumentNullException(nameof(type));
 
             if (_registeredRoutedEvents.TryGetValue(type, out var events))
             {

+ 0 - 1
src/Avalonia.Styling/Avalonia.Styling.csproj

@@ -8,5 +8,4 @@
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
   </ItemGroup>
-  <Import Project="..\..\build\Rx.props" />
 </Project>

+ 0 - 6
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

+ 65 - 52
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
 {
     /// <summary>
@@ -29,8 +29,8 @@ namespace Avalonia
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
         /// </summary>
-        public static readonly StyledProperty<object> DataContextProperty =
-            AvaloniaProperty.Register<StyledElement, object>(
+        public static readonly StyledProperty<object?> DataContextProperty =
+            AvaloniaProperty.Register<StyledElement, object?>(
                 nameof(DataContext),
                 inherits: true,
                 notifying: DataContextNotifying);
@@ -38,34 +38,34 @@ namespace Avalonia
         /// <summary>
         /// Defines the <see cref="Name"/> property.
         /// </summary>
-        public static readonly DirectProperty<StyledElement, string> NameProperty =
-            AvaloniaProperty.RegisterDirect<StyledElement, string>(nameof(Name), o => o.Name, (o, v) => o.Name = v);
+        public static readonly DirectProperty<StyledElement, string?> NameProperty =
+            AvaloniaProperty.RegisterDirect<StyledElement, string?>(nameof(Name), o => o.Name, (o, v) => o.Name = v);
         
         /// <summary>
         /// Defines the <see cref="Parent"/> property.
         /// </summary>
-        public static readonly DirectProperty<StyledElement, IStyledElement> ParentProperty =
-            AvaloniaProperty.RegisterDirect<StyledElement, IStyledElement>(nameof(Parent), o => o.Parent);
+        public static readonly DirectProperty<StyledElement, IStyledElement?> ParentProperty =
+            AvaloniaProperty.RegisterDirect<StyledElement, IStyledElement?>(nameof(Parent), o => o.Parent);
 
         /// <summary>
         /// Defines the <see cref="TemplatedParent"/> property.
         /// </summary>
-        public static readonly DirectProperty<StyledElement, ITemplatedControl> TemplatedParentProperty =
-            AvaloniaProperty.RegisterDirect<StyledElement, ITemplatedControl>(
+        public static readonly DirectProperty<StyledElement, ITemplatedControl?> TemplatedParentProperty =
+            AvaloniaProperty.RegisterDirect<StyledElement, ITemplatedControl?>(
                 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<ILogical> _logicalChildren;
-        private IResourceDictionary _resources;
-        private Styles _styles;
+        private ILogicalRoot? _logicalRoot;
+        private IAvaloniaList<ILogical>? _logicalChildren;
+        private IResourceDictionary? _resources;
+        private Styles? _styles;
         private bool _styled;
-        private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
-        private ITemplatedControl _templatedParent;
+        private List<IStyleInstance>? _appliedStyles;
+        private ITemplatedControl? _templatedParent;
         private bool _dataContextUpdating;
         private bool _notifyingResourcesChanged;
 
@@ -88,12 +88,12 @@ namespace Avalonia
         /// <summary>
         /// Raised when the styled element is attached to a rooted logical tree.
         /// </summary>
-        public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
+        public event EventHandler<LogicalTreeAttachmentEventArgs>? AttachedToLogicalTree;
 
         /// <summary>
         /// Raised when the styled element is detached from a rooted logical tree.
         /// </summary>
-        public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
+        public event EventHandler<LogicalTreeAttachmentEventArgs>? DetachedFromLogicalTree;
 
         /// <summary>
         /// Occurs when the <see cref="DataContext"/> property changes.
@@ -102,7 +102,7 @@ namespace Avalonia
         /// This event will be raised when the <see cref="DataContext"/> property has changed and
         /// all subscribers to that change have been notified.
         /// </remarks>
-        public event EventHandler DataContextChanged;
+        public event EventHandler? DataContextChanged;
 
         /// <summary>
         /// Occurs when the styled element has finished initialization.
@@ -115,12 +115,12 @@ namespace Avalonia
         /// <see cref="ISupportInitialize"/> is not used, it is called when the styled element is attached
         /// to the visual tree.
         /// </remarks>
-        public event EventHandler Initialized;
+        public event EventHandler? Initialized;
 
         /// <summary>
         /// Occurs when a resource in this styled element or a parent styled element has changed.
         /// </summary>
-        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+        public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
 
         /// <summary>
         /// Gets or sets the name of the styled element.
@@ -129,20 +129,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.
         /// </remarks>
-        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.");
@@ -190,7 +182,7 @@ namespace Avalonia
         /// The data context is an inherited property that specifies the default object that will
         /// be used for data binding.
         /// </remarks>
-        public object DataContext
+        public object? DataContext
         {
             get { return GetValue(DataContextProperty); }
             set { SetValue(DataContextProperty, value); }
@@ -217,7 +209,7 @@ namespace Avalonia
         {
             get
             {
-                if (_styles == null)
+                if (_styles is null)
                 {
                     _styles = new Styles(this);
                     _styles.ResourcesChanged += ThisResourcesChanged;
@@ -235,7 +227,7 @@ namespace Avalonia
             get => _resources ?? (Resources = new ResourceDictionary());
             set
             {
-                Contract.Requires<ArgumentNullException>(value != null);
+                value = value ?? throw new ArgumentNullException(nameof(value));
 
                 var hadResources = false;
 
@@ -264,7 +256,7 @@ namespace Avalonia
         /// <summary>
         /// Gets the styled element whose lookless template this styled element is part of.
         /// </summary>
-        public ITemplatedControl TemplatedParent
+        public ITemplatedControl? TemplatedParent
         {
             get => _templatedParent;
             internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
@@ -306,12 +298,12 @@ namespace Avalonia
         /// <summary>
         /// Gets the styled element's logical parent.
         /// </summary>
-        public IStyledElement Parent { get; private set; }
+        public IStyledElement? Parent { get; private set; }
 
         /// <summary>
         /// Gets the styled element's logical parent.
         /// </summary>
-        ILogical ILogical.LogicalParent => Parent;
+        ILogical? ILogical.LogicalParent => Parent;
 
         /// <summary>
         /// Gets the styled element's logical children.
@@ -322,7 +314,7 @@ namespace Avalonia
         bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources;
 
         /// <inheritdoc/>
-        IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
+        IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
 
         /// <inheritdoc/>
         IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
@@ -338,14 +330,11 @@ namespace Avalonia
         /// </remarks>
         Type IStyleable.StyleKey => GetType();
 
-        /// <inheritdoc/>
-        IObservable<IStyleable> IStyleable.StyleDetach => _styleDetach;
-
         /// <inheritdoc/>
         bool IStyleHost.IsStylesInitialized => _styles != null;
 
         /// <inheritdoc/>
-        IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
+        IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
 
         /// <inheritdoc/>
         public virtual void BeginInit()
@@ -391,20 +380,20 @@ namespace Avalonia
         /// <inheritdoc/>
         void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
-            this.OnAttachedToLogicalTreeCore(e);
+            OnAttachedToLogicalTreeCore(e);
         }
 
         /// <inheritdoc/>
         void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
-            this.OnDetachedFromLogicalTreeCore(e);
+            OnDetachedFromLogicalTreeCore(e);
         }
 
         /// <inheritdoc/>
         void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
 
         /// <inheritdoc/>
-        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) ||
@@ -415,7 +404,7 @@ namespace Avalonia
         /// Sets the styled element's logical parent.
         /// </summary>
         /// <param name="parent">The parent.</param>
-        void ISetLogicalParent.SetParent(ILogical parent)
+        void ISetLogicalParent.SetParent(ILogical? parent)
         {
             var old = Parent;
 
@@ -431,7 +420,7 @@ namespace Avalonia
                     InheritanceParent = parent as AvaloniaObject;
                 }
 
-                Parent = (IStyledElement)parent;
+                Parent = (IStyledElement?)parent;
 
                 if (_logicalRoot != null)
                 {
@@ -462,12 +451,13 @@ namespace Avalonia
                     var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent);
                     OnAttachedToLogicalTreeCore(e);
                 }
-
+#nullable disable
                 RaisePropertyChanged(
                     ParentProperty,
                     new Optional<IStyledElement>(old),
                     new BindingValue<IStyledElement>(Parent),
                     BindingPriority.LocalValue);
+#nullable enable
             }
         }
 
@@ -480,6 +470,16 @@ namespace Avalonia
             InheritanceParent = parent;
         }
 
+        void IStyleable.StyleApplied(IStyleInstance instance)
+        {
+            instance = instance ?? throw new ArgumentNullException(nameof(instance));
+
+            _appliedStyles ??= new List<IStyleInstance>();
+            _appliedStyles.Add(instance);
+        }
+
+        void IStyleable.DetachStyles() => DetachStyles();
+
         protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             switch (e.Action)
@@ -589,7 +589,7 @@ namespace Avalonia
             }
         }
 
-        private static ILogicalRoot FindLogicalRoot(IStyleHost e)
+        private static ILogicalRoot? FindLogicalRoot(IStyleHost e)
         {
             while (e != null)
             {
@@ -658,7 +658,7 @@ namespace Avalonia
             if (_logicalRoot != null)
             {
                 _logicalRoot = null;
-                _styleDetach.OnNext(this);
+                DetachStyles();
                 OnDetachedFromLogicalTree(e);
                 DetachedFromLogicalTree?.Invoke(this, e);
 
@@ -674,7 +674,7 @@ namespace Avalonia
                 }
 
 #if DEBUG
-                if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0)
+                if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0)
                 {
                     Logger.TryGet(LogEventLevel.Warning)?.Log(
                         LogArea.Control,
@@ -702,6 +702,19 @@ namespace Avalonia
             }
         }
 
+        private void DetachStyles()
+        {
+            if (_appliedStyles is object)
+            {
+                foreach (var i in _appliedStyles)
+                {
+                    i.Dispose();
+                }
+
+                _appliedStyles.Clear();
+            }
+        }
+
         private void ClearLogicalParent(IEnumerable<ILogical> children)
         {
             foreach (var i in children)

+ 0 - 77
src/Avalonia.Styling/Styling/ActivatedObservable.cs

@@ -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
-{
-    /// <summary>
-    /// An observable which is switched on or off according to an activator observable.
-    /// </summary>
-    /// <remarks>
-    /// An <see cref="ActivatedObservable"/> has two inputs: an activator observable and a 
-    /// <see cref="Source"/> observable which produces the activated value. When the activator 
-    /// produces true, the <see cref="ActivatedObservable"/> will produce the current activated 
-    /// value. When the activator produces false it will produce
-    /// <see cref="AvaloniaProperty.UnsetValue"/>.
-    /// </remarks>
-    internal class ActivatedObservable : ActivatedValue, IDescription
-    {
-        private IDisposable _sourceSubscription;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
-        /// </summary>
-        /// <param name="activator">The activator.</param>
-        /// <param name="source">An observable that produces the activated value.</param>
-        /// <param name="description">The binding description.</param>
-        public ActivatedObservable(
-            IObservable<bool> activator,
-            IObservable<object> source,
-            string description)
-            : base(activator, AvaloniaProperty.UnsetValue, description)
-        {
-            Contract.Requires<ArgumentNullException>(source != null);
-
-            Source = source;
-        }
-
-        /// <summary>
-        /// Gets an observable which produces the <see cref="ActivatedValue"/>.
-        /// </summary>
-        public IObservable<object> 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<object>
-        {
-            public ValueListener(ActivatedObservable parent)
-                : base(parent)
-            {
-            }
-            protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent;
-
-            void IObserver<object>.OnCompleted() => Parent.CompletedReceived();
-            void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error);
-            void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value);
-        }
-    }
-}

+ 0 - 110
src/Avalonia.Styling/Styling/ActivatedSubject.cs

@@ -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
-{
-    /// <summary>
-    /// A subject which is switched on or off according to an activator observable.
-    /// </summary>
-    /// <remarks>
-    /// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
-    /// be an <see cref="ISubject{Object}"/>. When the object is active then values
-    /// received via <see cref="OnNext(object)"/> will be passed to the source subject.
-    /// </remarks>
-    internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription
-    {
-        private bool _completed;
-        private object _pushValue;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
-        /// </summary>
-        /// <param name="activator">The activator.</param>
-        /// <param name="source">An observable that produces the activated value.</param>
-        /// <param name="description">The binding description.</param>
-        public ActivatedSubject(
-            IObservable<bool> activator,
-            ISubject<object> source,
-            string description)
-            : base(activator, source, description)
-        {
-        }
-
-        /// <summary>
-        /// Gets the underlying subject.
-        /// </summary>
-        public new ISubject<object> Source
-        {
-            get { return (ISubject<object>)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);
-        }
-    }
-}

+ 0 - 133
src/Avalonia.Styling/Styling/ActivatedValue.cs

@@ -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
-{
-    /// <summary>
-    /// An value which is switched on or off according to an activator observable.
-    /// </summary>
-    /// <remarks>
-    /// An <see cref="ActivatedValue"/> has two inputs: an activator observable and an
-    /// <see cref="Value"/>. When the activator produces true, the 
-    /// <see cref="ActivatedValue"/> will produce the current value. When the activator 
-    /// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
-    /// </remarks>
-    internal class ActivatedValue : LightweightObservableBase<object>, IDescription
-    {
-        private static readonly object NotSent = new object();
-        private IDisposable _activatorSubscription;
-        private object _value;
-        private object _last = NotSent;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
-        /// </summary>
-        /// <param name="activator">The activator.</param>
-        /// <param name="value">The activated value.</param>
-        /// <param name="description">The binding description.</param>
-        public ActivatedValue(
-            IObservable<bool> activator,
-            object value,
-            string description)
-        {
-            Contract.Requires<ArgumentNullException>(activator != null);
-
-            Activator = activator;
-            Value = value;
-            Description = description;
-            Listener = CreateListener();
-        }
-
-        /// <summary>
-        /// Gets the activator observable.
-        /// </summary>
-        public IObservable<bool> Activator { get; }
-
-        /// <summary>
-        /// Gets a description of the binding.
-        /// </summary>
-        public string Description { get; }
-
-        /// <summary>
-        /// Gets a value indicating whether the activator is active.
-        /// </summary>
-        public bool? IsActive { get; private set; }
-
-        /// <summary>
-        /// Gets the value that will be produced when <see cref="IsActive"/> is true.
-        /// </summary>
-        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<object> 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<bool>
-        {
-            public ActivatorListener(ActivatedValue parent)
-            {
-                Parent = parent;
-            }
-
-            protected ActivatedValue Parent { get; }
-
-            void IObserver<bool>.OnCompleted() => Parent.CompletedReceived();
-            void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error);
-            void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value);
-        }
-    }
-}

+ 71 - 0
src/Avalonia.Styling/Styling/Activators/AndActivator.cs

@@ -0,0 +1,71 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An aggregate <see cref="IStyleActivator"/> which is active when all of its inputs are
+    /// active.
+    /// </summary>
+    internal class AndActivator : StyleActivatorBase, IStyleActivatorSink
+    {
+        private List<IStyleActivator>? _sources;
+        private ulong _flags;
+        private ulong _mask;
+
+        public int Count => _sources?.Count ?? 0;
+
+        public void Add(IStyleActivator activator)
+        {
+            _sources ??= new List<IStyleActivator>();
+            _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;
+        }
+    }
+}

+ 43 - 0
src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs

@@ -0,0 +1,43 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// Builds an <see cref="AndActivator"/>.
+    /// </summary>
+    /// <remarks>
+    /// When ANDing style activators, if there is more than one input then creates an instance of
+    /// <see cref="AndActivator"/>. If there is only one input, returns the input directly.
+    /// </remarks>
+    internal struct AndActivatorBuilder
+    {
+        private IStyleActivator? _single;
+        private AndActivator? _multiple;
+
+        public void Add(IStyleActivator? activator)
+        {
+            if (activator == null)
+            {
+                return;
+            }
+
+            if (_single is null && _multiple is null)
+            {
+                _single = activator;
+            }
+            else
+            {
+                if (_multiple is null)
+                {
+                    _multiple = new AndActivator();
+                    _multiple.Add(_single!);
+                    _single = null;
+                }
+
+                _multiple.Add(activator);
+            }
+        }
+
+        public IStyleActivator Get() => _single ?? _multiple!;
+    }
+}

+ 33 - 0
src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs

@@ -0,0 +1,33 @@
+#nullable enable
+
+using System;
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// Defines a style activator.
+    /// </summary>
+    /// <remarks>
+    /// 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.
+    /// </remarks>
+    public interface IStyleActivator : IDisposable
+    {
+        /// <summary>
+        /// Subscribes to the activator.
+        /// </summary>
+        /// <param name="sink">The listener.</param>
+        /// <param name="tag">An optional tag.</param>
+        void Subscribe(IStyleActivatorSink sink, int tag = 0);
+
+        /// <summary>
+        /// Unsubscribes from the activator.
+        /// </summary>
+        void Unsubscribe(IStyleActivatorSink sink);
+    }
+}

+ 17 - 0
src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs

@@ -0,0 +1,17 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// Receives notifications from an <see cref="IStyleActivator"/>.
+    /// </summary>
+    public interface IStyleActivatorSink
+    {
+        /// <summary>
+        /// Called when the subscribed activator value changes.
+        /// </summary>
+        /// <param name="value">The new value.</param>
+        /// <param name="tag">The subscription tag.</param>
+        void OnNext(bool value, int tag);
+    }
+}

+ 16 - 0
src/Avalonia.Styling/Styling/Activators/NotActivator.cs

@@ -0,0 +1,16 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
+    /// </summary>
+    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);
+    }
+}

+ 71 - 0
src/Avalonia.Styling/Styling/Activators/OrActivator.cs

@@ -0,0 +1,71 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An aggregate <see cref="IStyleActivator"/> which is active when any of its inputs are
+    /// active.
+    /// </summary>
+    internal class OrActivator : StyleActivatorBase, IStyleActivatorSink
+    {
+        private List<IStyleActivator>? _sources;
+        private ulong _flags;
+        private bool _initializing;
+
+        public int Count => _sources?.Count ?? 0;
+
+        public void Add(IStyleActivator activator)
+        {
+            _sources ??= new List<IStyleActivator>();
+            _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);
+                }
+            }
+        }
+    }
+}

+ 45 - 0
src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs

@@ -0,0 +1,45 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// Builds an <see cref="OrActivator"/>.
+    /// </summary>
+    /// <remarks>
+    /// When ORing style activators, if there is more than one input then creates an instance of
+    /// <see cref="OrActivator"/>. If there is only one input, returns the input directly.
+    /// </remarks>
+    internal struct OrActivatorBuilder
+    {
+        private IStyleActivator? _single;
+        private OrActivator? _multiple;
+
+        public int Count => _multiple?.Count ?? (_single is object ? 1 : 0);
+
+        public void Add(IStyleActivator? activator)
+        {
+            if (activator == null)
+            {
+                return;
+            }
+
+            if (_single is null && _multiple is null)
+            {
+                _single = activator;
+            }
+            else
+            {
+                if (_multiple is null)
+                {
+                    _multiple = new OrActivator();
+                    _multiple.Add(_single!);
+                    _single = null;
+                }
+
+                _multiple.Add(activator);
+            }
+        }
+
+        public IStyleActivator Get() => _single ?? _multiple!;
+    }
+}

+ 38 - 0
src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs

@@ -0,0 +1,38 @@
+using System;
+
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An <see cref="IStyleActivator"/> which listens to a property value on a control.
+    /// </summary>
+    internal class PropertyEqualsActivator : StyleActivatorBase, IObserver<object>
+    {
+        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<object>.OnCompleted() { }
+        void IObserver<object>.OnError(Exception error) { }
+        void IObserver<object>.OnNext(object value) => PublishNext(Equals(value, _value));
+    }
+}

+ 58 - 0
src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs

@@ -0,0 +1,58 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// Base class implementation of <see cref="IStyleActivator"/>.
+    /// </summary>
+    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();
+    }
+}

+ 76 - 0
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Collections;
+
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
+    /// control.
+    /// </summary>
+    internal sealed class StyleClassActivator : StyleActivatorBase
+    {
+        private readonly IList<string> _match;
+        private readonly IAvaloniaReadOnlyList<string> _classes;
+
+        public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
+        {
+            _classes = classes;
+            _match = match;
+        }
+
+        public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> 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);
+    }
+}

+ 11 - 14
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.");
         }
 
         /// <inheritdoc/>
@@ -29,7 +26,7 @@ namespace Avalonia.Styling
         public override bool InTemplate => _parent.InTemplate;
 
         /// <inheritdoc/>
-        public override Type TargetType => null;
+        public override Type? TargetType => null;
 
         public override string ToString()
         {
@@ -43,8 +40,8 @@ namespace Avalonia.Styling
 
         protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
         {
-            ILogical c = (ILogical)control;
-            List<IObservable<bool>> descendantMatches = new List<IObservable<bool>>();
+            var c = (ILogical)control;
+            var descendantMatches = new OrActivatorBuilder();
 
             while (c != null)
             {
@@ -67,7 +64,7 @@ namespace Avalonia.Styling
 
             if (descendantMatches.Count > 0)
             {
-                return new SelectorMatch(StyleActivator.Or(descendantMatches));
+                return new SelectorMatch(descendantMatches.Get());
             }
             else
             {
@@ -75,6 +72,6 @@ namespace Avalonia.Styling
             }
         }
 
-        protected override Selector MovePrevious() => null;
+        protected override Selector? MovePrevious() => null;
     }
 }

+ 12 - 6
src/Avalonia.Styling/Styling/ISetter.cs

@@ -3,6 +3,8 @@
 
 using System;
 
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -11,11 +13,15 @@ namespace Avalonia.Styling
     public interface ISetter
     {
         /// <summary>
-        /// Applies the setter to a control.
+        /// Instances a setter on a control.
         /// </summary>
-        /// <param name="style">The style that is being applied.</param>
-        /// <param name="control">The control.</param>
-        /// <param name="activator">An optional activator.</param>
-        IDisposable Apply(IStyle style, IStyleable control, IObservable<bool> activator);
+        /// <param name="target">The control.</param>
+        /// <returns>An <see cref="ISetterInstance"/>.</returns>
+        /// <remarks>
+        /// This method should return an <see cref="ISetterInstance"/> which can be used to apply
+        /// the setter to the specified control. Note that it should not apply the setter value 
+        /// until <see cref="ISetterInstance.Start(bool)"/> is called.
+        /// </remarks>
+        ISetterInstance Instance(IStyleable target);
     }
-}
+}

+ 40 - 0
src/Avalonia.Styling/Styling/ISetterInstance.cs

@@ -0,0 +1,40 @@
+#nullable enable
+
+using System;
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// Represents a setter that has been instanced on a control.
+    /// </summary>
+    public interface ISetterInstance : IDisposable
+    {
+        /// <summary>
+        /// Starts the setter instance.
+        /// </summary>
+        /// <param name="hasActivator">Whether the parent style has an activator.</param>
+        /// <remarks>
+        /// If <paramref name="hasActivator"/> is false then the setter should be immediately
+        /// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
+        /// If true, then bindings etc should be initiated but not produce a value until
+        /// <see cref="Activate"/> called.
+        /// </remarks>
+        public void Start(bool hasActivator);
+
+        /// <summary>
+        /// Activates the setter.
+        /// </summary>
+        /// <remarks>
+        /// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
+        /// </remarks>
+        public void Activate();
+
+        /// <summary>
+        /// Deactivates the setter.
+        /// </summary>
+        /// <remarks>
+        /// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
+        /// </remarks>
+        public void Deactivate();
+    }
+}

+ 6 - 10
src/Avalonia.Styling/Styling/IStyle.cs

@@ -3,6 +3,8 @@
 
 using Avalonia.Controls;
 
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -13,17 +15,11 @@ namespace Avalonia.Styling
         /// <summary>
         /// Attaches the style to a control if the style's selector matches.
         /// </summary>
-        /// <param name="control">The control to attach to.</param>
-        /// <param name="container">
-        /// The control that contains this style. May be null.
-        /// </param>
+        /// <param name="target">The control to attach to.</param>
+        /// <param name="host">The element that hosts the style.</param>
         /// <returns>
-        /// True if the style can match a control of type <paramref name="control"/>
-        /// (even if it does not match this control specifically); false if the style
-        /// can never match.
+        /// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
         /// </returns>
-        bool Attach(IStyleable control, IStyleHost container);
-
-        void Detach();
+        SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
     }
 }

+ 22 - 0
src/Avalonia.Styling/Styling/IStyleInstance.cs

@@ -0,0 +1,22 @@
+using System;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// Represents a style that has been instanced on a control.
+    /// </summary>
+    public interface IStyleInstance : IDisposable
+    {
+        /// <summary>
+        /// Gets the source style.
+        /// </summary>
+        IStyle Source { get; }
+
+        /// <summary>
+        /// Instructs the style to start acting upon the control.
+        /// </summary>
+        void Start();
+    }
+}

+ 14 - 6
src/Avalonia.Styling/Styling/IStyleable.cs

@@ -4,6 +4,8 @@
 using System;
 using Avalonia.Collections;
 
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -11,11 +13,6 @@ namespace Avalonia.Styling
     /// </summary>
     public interface IStyleable : IAvaloniaObject, INamed
     {
-        /// <summary>
-        /// Signaled when the control's style should be removed.
-        /// </summary>
-        IObservable<IStyleable> StyleDetach { get; }
-
         /// <summary>
         /// Gets the list of classes for the control.
         /// </summary>
@@ -29,6 +26,17 @@ namespace Avalonia.Styling
         /// <summary>
         /// Gets the template parent of this element if the control comes from a template.
         /// </summary>
-        ITemplatedControl TemplatedParent { get; }
+        ITemplatedControl? TemplatedParent { get; }
+
+        /// <summary>
+        /// Notifies the element that a style has been applied.
+        /// </summary>
+        /// <param name="instance">The style instance.</param>
+        void StyleApplied(IStyleInstance instance);
+
+        /// <summary>
+        /// Detaches all styles applied to the element.
+        /// </summary>
+        void DetachStyles();
     }
 }

+ 10 - 8
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
     /// </summary>
     internal class NotSelector : Selector
     {
-        private readonly Selector _previous;
+        private readonly Selector? _previous;
         private readonly Selector _argument;
-        private string _selectorString;
+        private string? _selectorString;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="NotSelector"/> class.
         /// </summary>
         /// <param name="previous">The previous selector.</param>
         /// <param name="argument">The selector to be not-ed.</param>
-        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,14 +35,14 @@ namespace Avalonia.Styling
         public override bool IsCombinator => false;
 
         /// <inheritdoc/>
-        public override Type TargetType => _previous?.TargetType;
+        public override Type? TargetType => _previous?.TargetType;
 
         /// <inheritdoc/>
         public override string ToString()
         {
             if (_selectorString == null)
             {
-                _selectorString = ":not(" + _argument.ToString() + ")";
+                _selectorString = $"{_previous?.ToString()}:not({_argument})";
             }
 
             return _selectorString;
@@ -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;
     }
 }

+ 22 - 16
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<Selector> _selectors;
-        private string _selectorString;
-        private Type _targetType;
+        private string? _selectorString;
+        private Type? _targetType;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="OrSelector"/> class.
@@ -21,8 +24,15 @@ namespace Avalonia.Styling
         /// <param name="selectors">The selectors to OR.</param>
         public OrSelector(IReadOnlyList<Selector> selectors)
         {
-            Contract.Requires<ArgumentNullException>(selectors != null);
-            Contract.Requires<ArgumentException>(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;
 
         /// <inheritdoc/>
-        public override Type TargetType
+        public override Type? TargetType
         {
             get
             {
@@ -60,7 +70,7 @@ namespace Avalonia.Styling
 
         protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
         {
-            var activators = new List<IObservable<bool>>();
+            var activators = new OrActivatorBuilder();
             var neverThisInstance = false;
 
             foreach (var selector in _selectors)
@@ -76,18 +86,14 @@ namespace Avalonia.Styling
                         neverThisInstance = true;
                         break;
                     case SelectorMatchResult.Sometimes:
-                        activators.Add(match.Activator);
+                        activators.Add(match.Activator!);
                         break;
                 }
             }
 
-            if (activators.Count > 1)
-            {
-                return new SelectorMatch(StyleActivator.Or(activators));
-            }
-            else if (activators.Count == 1)
+            if (activators.Count > 0)
             {
-                return new SelectorMatch(activators[0]);
+                return new SelectorMatch(activators.Get());
             }
             else if (neverThisInstance)
             {
@@ -99,11 +105,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)
             {

+ 11 - 14
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
     /// </summary>
     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<ArgumentNullException>(property != null);
+            property = property ?? throw new ArgumentNullException(nameof(property));
 
             _previous = previous;
             _property = property;
@@ -33,13 +35,8 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         public override bool IsCombinator => false;
 
-        /// <summary>
-        /// Gets the name of the control to match.
-        /// </summary>
-        public string Name { get; private set; }
-
         /// <inheritdoc/>
-        public override Type TargetType => _previous?.TargetType;
+        public override Type? TargetType => _previous?.TargetType;
 
         /// <inheritdoc/>
         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;
     }
 }

+ 180 - 0
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@@ -0,0 +1,180 @@
+using System;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// A <see cref="Setter"/> which has been instanced on a control and has an
+    /// <see cref="IBinding"/> as its value.
+    /// </summary>
+    /// <typeparam name="T">The target property type.</typeparam>
+    internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISubject<BindingValue<T>>,
+        ISetterInstance
+    {
+        private readonly IStyleable _target;
+        private readonly StyledPropertyBase<T>? _styledProperty;
+        private readonly DirectPropertyBase<T>? _directProperty;
+        private readonly InstancedBinding _binding;
+        private readonly Inner _inner;
+        private BindingValue<T> _value;
+        private IDisposable? _subscription;
+        private IDisposable? _subscriptionTwoWay;
+        private bool _isActive;
+
+        public PropertySetterBindingInstance(
+            IStyleable target,
+            StyledPropertyBase<T> property,
+            IBinding binding)
+        {
+            _target = target;
+            _styledProperty = property;
+            _binding = binding.Initiate(_target, property);
+
+            if (_binding.Mode == BindingMode.OneTime)
+            {
+                // For the moment, we don't support OneTime bindings in setters, because I'm not
+                // sure what the semantics should be in the case of activation/deactivation.
+                throw new NotSupportedException("OneTime bindings are not supported in setters.");
+            }
+
+            _inner = new Inner(this);
+        }
+
+        public PropertySetterBindingInstance(
+            IStyleable target,
+            DirectPropertyBase<T> property,
+            IBinding binding)
+        {
+            _target = target;
+            _directProperty = property;
+            _binding = binding.Initiate(_target, property);
+            _inner = new Inner(this);
+        }
+
+        public void Start(bool hasActivator)
+        {
+            _isActive = !hasActivator;
+
+            if (_styledProperty is object)
+            {
+                if (_binding.Mode != BindingMode.OneWayToSource)
+                {
+                    var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
+                    _subscription = _target.Bind(_styledProperty, this, priority);
+                }
+
+                if (_binding.Mode == BindingMode.TwoWay)
+                {
+                    _subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this);
+                }
+            }
+            else
+            {
+                if (_binding.Mode != BindingMode.OneWayToSource)
+                {
+                    _subscription = _target.Bind(_directProperty!, this);
+                }
+
+                if (_binding.Mode == BindingMode.TwoWay)
+                {
+                    _subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this);
+                }
+            }
+        }
+
+        public void Activate()
+        {
+            if (!_isActive)
+            {
+                _isActive = true;
+                PublishNext();
+            }
+        }
+
+        public void Deactivate()
+        {
+            if (_isActive)
+            {
+                _isActive = false;
+                PublishNext();
+            }
+        }
+
+        public override void Dispose()
+        {
+            if (_subscription is object)
+            {
+                var sub = _subscription;
+                _subscription = null;
+                sub.Dispose();
+            }
+
+            if (_subscriptionTwoWay is object)
+            {
+                var sub = _subscriptionTwoWay;
+                _subscriptionTwoWay = null;
+                sub.Dispose();
+            }
+
+            base.Dispose();
+        }
+
+        void IObserver<BindingValue<T>>.OnCompleted()
+        {
+            // This is the observable coming from the target control. It should not complete.
+        }
+
+        void IObserver<BindingValue<T>>.OnError(Exception error)
+        {
+            // This is the observable coming from the target control. It should not error.
+        }
+
+        void IObserver<BindingValue<T>>.OnNext(BindingValue<T> value)
+        {
+            if (value.HasValue && _isActive)
+            {
+                _binding.Subject.OnNext(value.Value);
+            }
+        }
+
+        protected override void Subscribed()
+        {
+            _subscription = _binding.Observable.Subscribe(_inner);
+        }
+
+        protected override void Unsubscribed()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+        }
+
+        private void PublishNext()
+        {
+            PublishNext(_isActive ? _value : default);
+        }
+
+        private void ConvertAndPublishNext(object? value)
+        {
+            _value = value is T v ? v : BindingValue<object>.FromUntyped(value).Convert<T>();
+
+            if (_isActive)
+            {
+                PublishNext();
+            }
+        }
+
+        private class Inner : IObserver<object?>
+        {
+            private readonly PropertySetterBindingInstance<T> _owner;
+            public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner;
+            public void OnCompleted() => _owner.PublishCompleted();
+            public void OnError(Exception error) => _owner.PublishError(error);
+            public void OnNext(object? value) => _owner.ConvertAndPublishNext(value);
+        }
+    }
+}

+ 118 - 0
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@@ -0,0 +1,118 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// A <see cref="Setter"/> which has been instance on a control.
+    /// </summary>
+    /// <typeparam name="T">The target property type.</typeparam>
+    internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISetterInstance
+    {
+        private readonly IStyleable _target;
+        private readonly StyledPropertyBase<T>? _styledProperty;
+        private readonly DirectPropertyBase<T>? _directProperty;
+        private readonly T _value;
+        private IDisposable? _subscription;
+        private bool _isActive;
+
+        public PropertySetterInstance(
+            IStyleable target,
+            StyledPropertyBase<T> property,
+            T value)
+        {
+            _target = target;
+            _styledProperty = property;
+            _value = value;
+        }
+
+        public PropertySetterInstance(
+            IStyleable target,
+            DirectPropertyBase<T> property,
+            T value)
+        {
+            _target = target;
+            _directProperty = property;
+            _value = value;
+        }
+
+        public void Start(bool hasActivator)
+        {
+            if (hasActivator)
+            {
+                if (_styledProperty is object)
+                {
+                    _subscription = _target.Bind(_styledProperty, this, BindingPriority.StyleTrigger);
+                }
+                else
+                {
+                    _subscription = _target.Bind(_directProperty, this);
+                }
+            }
+            else
+            {
+                if (_styledProperty is object)
+                {
+                    _subscription = _target.SetValue(_styledProperty, _value, BindingPriority.Style);
+                }
+                else
+                {
+                    _target.SetValue(_directProperty!, _value);
+                }
+            }
+        }
+
+        public void Activate()
+        {
+            if (!_isActive)
+            {
+                _isActive = true;
+                PublishNext();
+            }
+        }
+
+        public void Deactivate()
+        {
+            if (_isActive)
+            {
+                _isActive = false;
+                PublishNext();
+            }
+        }
+
+        public override void Dispose()
+        {
+            if (_subscription is object)
+            {
+                var sub = _subscription;
+                _subscription = null;
+                sub.Dispose();
+            }
+            else if (_isActive)
+            {
+                if (_styledProperty is object)
+                {
+                    _target.ClearValue(_styledProperty);
+                }
+                else
+                {
+                    _target.ClearValue(_directProperty);
+                }
+            }
+
+            base.Dispose();
+        }
+
+        protected override void Subscribed() => PublishNext();
+        protected override void Unsubscribed() { }
+
+        private void PublishNext()
+        {
+            PublishNext(_isActive ? new BindingValue<T>(_value) : default);
+        }
+    }
+}

+ 81 - 48
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
         /// <summary>
         /// Gets the target type of the selector, if available.
         /// </summary>
-        public abstract Type TargetType { get; }
+        public abstract Type? TargetType { get; }
 
         /// <summary>
         /// Tries to match the selector with a control.
@@ -43,54 +43,28 @@ namespace Avalonia.Styling
         /// <returns>A <see cref="SelectorMatch"/>.</returns>
         public SelectorMatch Match(IStyleable control, bool subscribe = true)
         {
-            ValueSingleOrList<IObservable<bool>> inputs = default;
-
-            var selector = this;
-            var alwaysThisType = true;
-            var hitCombinator = false;
-
-            while (selector != null)
+            // First match the selector until a combinator is found. Selectors are stored from 
+            // right-to-left, so MatchUntilCombinator reverses this order because the type selector
+            // will be on the left.
+            var match = MatchUntilCombinator(control, this, subscribe, out var combinator);
+            
+            // If the pre-combinator selector matches, we can now match the combinator, if any.
+            if (match.IsMatch && combinator is object)
             {
-                hitCombinator |= selector.IsCombinator;
+                match = match.And(combinator.Match(control, subscribe));
 
-                var match = selector.Evaluate(control, subscribe);
-
-                if (!match.IsMatch)
-                {
-                    return hitCombinator ? SelectorMatch.NeverThisInstance : match;
-                }
-                else if (selector.InTemplate && control.TemplatedParent == null)
+                // If we have a combinator then we can never say that we always match a control of
+                // this type, because by definition the combinator matches on things outside of the
+                // control.
+                match = match.Result switch
                 {
-                    return SelectorMatch.NeverThisInstance;
-                }
-                else if (match.Result == SelectorMatchResult.AlwaysThisInstance)
-                {
-                    alwaysThisType = false;
-                }
-                else if (match.Result == SelectorMatchResult.Sometimes)
-                {
-                    Debug.Assert(match.Activator != null);
-
-                    inputs.Add(match.Activator);
-                }
-
-                selector = selector.MovePrevious();
+                    SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance,
+                    SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance,
+                    _ => match
+                };
             }
 
-            if (inputs.HasList)
-            {
-                return new SelectorMatch(StyleActivator.And(inputs.List));
-            }
-            else if (inputs.IsSingle)
-            {
-                return new SelectorMatch(inputs.Single);
-            }
-            else
-            {
-                return alwaysThisType && !hitCombinator ?
-                    SelectorMatch.AlwaysThisType :
-                    SelectorMatch.AlwaysThisInstance;
-            }
+            return match;
         }
 
         /// <summary>
@@ -107,6 +81,65 @@ namespace Avalonia.Styling
         /// <summary>
         /// Moves to the previous selector.
         /// </summary>
-        protected abstract Selector MovePrevious();
+        protected abstract Selector? MovePrevious();
+
+        private static SelectorMatch MatchUntilCombinator(
+            IStyleable control,
+            Selector start,
+            bool subscribe,
+            out Selector? combinator)
+        {
+            combinator = null;
+
+            var activators = new AndActivatorBuilder();
+            var result = Match(control, start, subscribe, ref activators, ref combinator);
+
+            return result == SelectorMatchResult.Sometimes ?
+                new SelectorMatch(activators.Get()) :
+                new SelectorMatch(result);
+        }
+
+        private static SelectorMatchResult Match(
+            IStyleable control,
+            Selector selector,
+            bool subscribe,
+            ref AndActivatorBuilder activators,
+            ref Selector? combinator)
+        {
+            var previous = selector.MovePrevious();
+
+            // Selectors are stored from right-to-left, so we recurse into the selector in order to
+            // reverse this order, because the type selector will be on the left and is our best
+            // opportunity to exit early.
+            if (previous != null && !previous.IsCombinator)
+            {
+                var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator);
+
+                if (previousMatch < SelectorMatchResult.Sometimes)
+                {
+                    return previousMatch;
+                }
+            }
+
+            // Match this selector.
+            var match = selector.Evaluate(control, subscribe);
+
+            if (!match.IsMatch)
+            {
+                combinator = null;
+                return match.Result;
+            }
+            else if (match.Activator is object)
+            {
+                activators.Add(match.Activator!);
+            }
+
+            if (previous?.IsCombinator == true)
+            {
+                combinator = previous;
+            }
+
+            return match.Result;
+        }
     }
 }

+ 47 - 11
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,
 
         /// <summary>
-        /// The selector always matches this type.
+        /// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>.
         /// </summary>
-        AlwaysThisType,
+        Sometimes,
 
         /// <summary>
         /// The selector always matches this instance, but doesn't always match this type.
@@ -31,9 +34,9 @@ namespace Avalonia.Styling
         AlwaysThisInstance,
 
         /// <summary>
-        /// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>.
+        /// The selector always matches this type.
         /// </summary>
-        Sometimes,
+        AlwaysThisType,
     }
 
     /// <summary>
@@ -43,7 +46,7 @@ namespace Avalonia.Styling
     /// A selector match describes whether and how a <see cref="Selector"/> matches a control, and
     /// in addition whether the selector can ever match a control of the same type.
     /// </remarks>
-    public class SelectorMatch
+    public readonly struct SelectorMatch
     {
         /// <summary>
         /// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>.
@@ -70,20 +73,28 @@ namespace Avalonia.Styling
         /// <see cref="SelectorMatchResult.Sometimes"/> result.
         /// </summary>
         /// <param name="match">The match activator.</param>
-        public SelectorMatch(IObservable<bool> match)
+        public SelectorMatch(IStyleActivator match)
         {
-            Contract.Requires<ArgumentNullException>(match != null);
+            match = match ?? throw new ArgumentNullException(nameof(match));
 
             Result = SelectorMatchResult.Sometimes;
             Activator = match;
         }
 
-        private SelectorMatch(SelectorMatchResult result) => Result = result;
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SelectorMatch"/> class with the specified result.
+        /// </summary>
+        /// <param name="result">The match result.</param>
+        public SelectorMatch(SelectorMatchResult result)
+        {
+            Result = result;
+            Activator = null;
+        }
 
         /// <summary>
         /// Gets a value indicating whether the match was positive.
         /// </summary>
-        public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType;
+        public bool IsMatch => Result >= SelectorMatchResult.Sometimes;
 
         /// <summary>
         /// Gets the result of the match.
@@ -91,9 +102,34 @@ namespace Avalonia.Styling
         public SelectorMatchResult Result { get; }
 
         /// <summary>
-        /// 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.
         /// </summary>
-        public IObservable<bool> Activator { get; }
+        public IStyleActivator? Activator { get; }
+
+        /// <summary>
+        /// Logical ANDs this <see cref="SelectorMatch"/> with another.
+        /// </summary>
+        /// <param name="other"></param>
+        /// <returns></returns>
+        public SelectorMatch And(in SelectorMatch other)
+        {
+            var result = (SelectorMatchResult)Math.Min((int)Result, (int)other.Result);
+
+            if (result == SelectorMatchResult.Sometimes)
+            {
+                var activators = new AndActivatorBuilder();
+                activators.Add(Activator);
+                activators.Add(other.Activator);
+                return new SelectorMatch(activators.Get());
+            }
+            else
+            {
+                return new SelectorMatch(result);
+            }
+        }
+
+        /// <inheritdoc/>
+        public override string ToString() => Result.ToString();
     }
 }

+ 59 - 87
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 <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
     /// <see cref="AvaloniaObject"/> depending on a condition.
     /// </remarks>
-    public class Setter : ISetter, IAnimationSetter
+    public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor<Setter.SetterVisitorData>
     {
-        private object _value;
+        private object? _value;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Setter"/> class.
@@ -42,11 +43,7 @@ namespace Avalonia.Styling
         /// <summary>
         /// Gets or sets the property to set.
         /// </summary>
-        public AvaloniaProperty Property
-        {
-            get;
-            set;
-        }
+        public AvaloniaProperty? Property { get; set; }
 
         /// <summary>
         /// 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,78 @@ namespace Avalonia.Styling
             }
         }
 
-        /// <summary>
-        /// Applies the setter to a control.
-        /// </summary>
-        /// <param name="style">The style that is being applied.</param>
-        /// <param name="control">The control.</param>
-        /// <param name="activator">An optional activator.</param>
-        public IDisposable Apply(IStyle style, IStyleable control, IObservable<bool> activator)
+        public ISetterInstance Instance(IStyleable target)
         {
-            Contract.Requires<ArgumentNullException>(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;
 
-            if (binding == null)
+            if (value is ITemplate template &&
+                !typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
             {
-                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);
-                }
+                value = template.Build();
             }
-            else
-            {
-                var source = binding.Initiate(control, Property);
 
-                if (source != null)
-                {
-                    var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
-                    return BindingOperations.Apply(control, Property, cloned, null);
-                }
-            }
+            var data = new SetterVisitorData
+            {
+                target = target,
+                value = value,
+            };
 
-            return Disposable.Empty;
+            Property.Accept(this, ref data);
+            return data.result!;
         }
 
-        private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
+        void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
+            StyledPropertyBase<T> property,
+            ref SetterVisitorData data)
         {
-            if (activator != null)
+            if (data.value is IBinding binding)
+            {
+                data.result = new PropertySetterBindingInstance<T>(
+                    data.target,
+                    property,
+                    binding);
+            }
+            else
             {
-                var description = style?.ToString();
-
-                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);
-                        }
-                }
+                data.result = new PropertySetterInstance<T>(
+                    data.target,
+                    property,
+                    (T)data.value);
+            }
+        }
 
+        void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
+            DirectPropertyBase<T> property,
+            ref SetterVisitorData data)
+        {
+            if (data.value is IBinding binding)
+            {
+                data.result = new PropertySetterBindingInstance<T>(
+                    data.target,
+                    property,
+                    binding);
             }
             else
             {
-                return sourceInstance.WithPriority(BindingPriority.Style);
+                data.result = new PropertySetterInstance<T>(
+                    data.target,
+                    property,
+                    (T)data.value);
             }
         }
+
+        private struct SetterVisitorData
+        {
+            public IStyleable target;
+            public object? value;
+            public ISetterInstance? result;
+        }
     }
 }

+ 27 - 141
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
 {
     /// <summary>
@@ -16,15 +16,10 @@ namespace Avalonia.Styling
     /// </summary>
     public class Style : AvaloniaObject, IStyle, ISetResourceParent
     {
-        private static Dictionary<IStyleable, CompositeDisposable> _applied =
-            new Dictionary<IStyleable, CompositeDisposable>();
-        private IResourceNode _parent;
-
-        private CompositeDisposable _subscriptions;
-
-        private IResourceDictionary _resources;
-
-        private IList<IAnimation> _animations;
+        private IResourceNode? _parent;
+        private IResourceDictionary? _resources;
+        private List<ISetter>? _setters;
+        private List<IAnimation>? _animations;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Style"/> class.
@@ -37,13 +32,13 @@ namespace Avalonia.Styling
         /// Initializes a new instance of the <see cref="Style"/> class.
         /// </summary>
         /// <param name="selector">The style selector.</param>
-        public Style(Func<Selector, Selector> selector)
+        public Style(Func<Selector?, Selector> selector)
         {
             Selector = selector(null);
         }
 
         /// <inheritdoc/>
-        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+        public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
 
         /// <summary>
         /// Gets or sets a dictionary of style resources.
@@ -53,7 +48,7 @@ namespace Avalonia.Styling
             get => _resources ?? (Resources = new ResourceDictionary());
             set
             {
-                Contract.Requires<ArgumentNullException>(value != null);
+                value = value ?? throw new ArgumentNullException(nameof(value));
 
                 var hadResources = false;
 
@@ -76,117 +71,45 @@ namespace Avalonia.Styling
         /// <summary>
         /// Gets or sets the style's selector.
         /// </summary>
-        public Selector Selector { get; set; }
+        public Selector? Selector { get; set; }
 
         /// <summary>
-        /// Gets or sets the style's setters.
+        /// Gets the style's setters.
         /// </summary>
         [Content]
-        public IList<ISetter> Setters { get; set; } = new List<ISetter>();
-
-        public IList<IAnimation> Animations
-        {
-            get
-            {
-                return _animations ?? (_animations = new List<IAnimation>());
-            }
-        }
+        public IList<ISetter> Setters => _setters ??= new List<ISetter>();
 
-        private CompositeDisposable Subscriptions
-        {
-            get
-            {
-                return _subscriptions ?? (_subscriptions = new CompositeDisposable(2));
-            }
-        }
+        /// <summary>
+        /// Gets the style's animations.
+        /// </summary>
+        public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
 
         /// <inheritdoc/>
-        IResourceNode IResourceNode.ResourceParent => _parent;
+        IResourceNode? IResourceNode.ResourceParent => _parent;
 
         /// <inheritdoc/>
         bool IResourceProvider.HasResources => _resources?.Count > 0;
 
         /// <inheritdoc/>
-        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;
+            target = target ?? throw new ArgumentNullException(nameof(target));
 
-                    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);
-                    }
-
-                    subs.Add(Disposable.Create((subs, Subscriptions) , state =>  state.Subscriptions.Remove(state.subs)));
-
-                    controlSubscriptions.Add(subs);
-                    Subscriptions.Add(subs);
-                }
+            var match = Selector is object ? Selector.Match(target) :
+                target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
 
-                return match.Result != SelectorMatchResult.NeverThisType;
-            }
-            else if (control == container)
+            if (match.IsMatch && (_setters is object || _animations 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, _animations, match.Activator);
+                target.StyleApplied(instance);
+                instance.Start();
             }
 
-            return false;
+            return match.Result;
         }
 
         /// <inheritdoc/>
-        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;
@@ -222,46 +145,9 @@ namespace Avalonia.Styling
                 throw new InvalidOperationException("The Style already has a parent.");
             }
 
-            if (parent == null)
-            {
-                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;
-        }
-
-        /// <summary>
-        /// Called when a control's <see cref="IStyleable.StyleDetach"/> is signaled to remove
-        /// all applied styles.
-        /// </summary>
-        /// <param name="control">The control.</param>
-        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);

+ 0 - 56
src/Avalonia.Styling/Styling/StyleActivator.cs

@@ -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<bool> And(IList<IObservable<bool>> 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<bool> Or(IList<IObservable<bool>> 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();
-            }
-        }
-    }
-}

+ 135 - 0
src/Avalonia.Styling/Styling/StyleInstance.cs

@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Subjects;
+using Avalonia.Animation;
+using Avalonia.Styling.Activators;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// A <see cref="Style"/> which has been instanced on a control.
+    /// </summary>
+    internal class StyleInstance : IStyleInstance, IStyleActivatorSink
+    {
+        private readonly List<ISetterInstance>? _setters;
+        private readonly List<IDisposable>? _animations;
+        private readonly IStyleActivator? _activator;
+        private readonly Subject<bool>? _animationTrigger;
+        private bool _active;
+
+        public StyleInstance(
+            IStyle source,
+            IStyleable target,
+            IReadOnlyList<ISetter>? setters,
+            IReadOnlyList<IAnimation>? animations,
+            IStyleActivator? activator = null)
+        {
+            Source = source ?? throw new ArgumentNullException(nameof(source));
+            Target = target ?? throw new ArgumentNullException(nameof(target));
+            _activator = activator;
+
+            if (setters is object)
+            {
+                var setterCount = setters.Count;
+
+                _setters = new List<ISetterInstance>(setterCount);
+
+                for (var i = 0; i < setterCount; ++i)
+                {
+                    _setters.Add(setters[i].Instance(Target));
+                }
+            }
+
+            if (animations is object && target is Animatable animatable)
+            {
+                var animationsCount = animations.Count;
+
+                _animations = new List<IDisposable>(animationsCount);
+                _animationTrigger = new Subject<bool>();
+
+                for (var i = 0; i < animationsCount; ++i)
+                {
+                    _animations.Add(animations[i].Apply(animatable, null, _animationTrigger));
+                }
+            }
+        }
+
+        public IStyle Source { get; }
+        public IStyleable Target { get; }
+
+        public void Start()
+        {
+            var hasActivator = _activator is object;
+
+            if (_setters is object)
+            {
+                foreach (var setter in _setters)
+                {
+                    setter.Start(hasActivator);
+                }
+            }
+
+            if (hasActivator)
+            {
+                _activator!.Subscribe(this, 0);
+            }
+            else if (_animationTrigger != null)
+            {
+                _animationTrigger.OnNext(true);
+            }
+        }
+
+        public void Dispose()
+        {
+            if (_setters is object)
+            {
+                foreach (var setter in _setters)
+                {
+                    setter.Dispose();
+                }
+            }
+
+            if (_animations is object)
+            {
+                foreach (var subscripion in _animations)
+                {
+                    subscripion.Dispose();
+                }
+            }
+
+            _activator?.Dispose();
+        }
+
+        private void ActivatorChanged(bool value)
+        {
+            if (_active != value)
+            {
+                _active = value;
+
+                _animationTrigger?.OnNext(value);
+
+                if (_setters is object)
+                {
+                    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);
+    }
+}

+ 12 - 13
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<ArgumentNullException>(control != null);
-            Contract.Requires<ArgumentNullException>(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);
             }
         }
     }

+ 31 - 45
src/Avalonia.Styling/Styling/Styles.cs

@@ -9,6 +9,8 @@ using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls;
 
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -16,10 +18,10 @@ namespace Avalonia.Styling
     /// </summary>
     public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent
     {
-        private IResourceNode _parent;
-        private IResourceDictionary _resources;
-        private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
-        private Dictionary<Type, List<IStyle>> _cache;
+        private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
+        private IResourceNode? _parent;
+        private IResourceDictionary? _resources;
+        private Dictionary<Type, List<IStyle>?>? _cache;
         private bool _notifyingResourcesChanged;
 
         public Styles()
@@ -74,7 +76,7 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+        public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
 
         /// <inheritdoc/>
         public int Count => _styles.Count;
@@ -90,7 +92,7 @@ namespace Avalonia.Styling
             get => _resources ?? (Resources = new ResourceDictionary());
             set
             {
-                Contract.Requires<ArgumentNullException>(value != null);
+                value = value ?? throw new ArgumentNullException(nameof(Resources));
 
                 var hadResources = false;
 
@@ -111,7 +113,7 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        IResourceNode IResourceNode.ResourceParent => _parent;
+        IResourceNode? IResourceNode.ResourceParent => _parent;
 
         /// <inheritdoc/>
         bool ICollection<IStyle>.IsReadOnly => false;
@@ -126,66 +128,50 @@ namespace Avalonia.Styling
             set => _styles[index] = value;
         }
 
-        /// <summary>
-        /// Attaches the style to a control if the style's selector matches.
-        /// </summary>
-        /// <param name="control">The control to attach to.</param>
-        /// <param name="container">
-        /// The control that contains this style. May be null.
-        /// </param>
-        public bool Attach(IStyleable control, IStyleHost container)
+        /// <inheritdoc/>
+        public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
         {
-            if (_cache == null)
-            {
-                _cache = new Dictionary<Type, List<IStyle>>();
-            }
+            _cache ??= new Dictionary<Type, List<IStyle>?>();
 
-            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<IStyle> result = null;
+                List<IStyle>? 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<IStyle>();
-                        }
-
-                        result.Add(style);
+                        matches ??= new List<IStyle>();
+                        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;
             }
         }
 
         /// <inheritdoc/>
-        public bool TryGetResource(object key, out object value)
+        public bool TryGetResource(object key, out object? value)
         {
             if (_resources != null && _resources.TryGetResource(key, out value))
             {

+ 2 - 3
src/Avalonia.Styling/Styling/TemplateSelector.cs

@@ -41,12 +41,11 @@ namespace Avalonia.Styling
 
         protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
         {
-            IStyleable templatedParent = control.TemplatedParent as IStyleable;
+            var templatedParent = control.TemplatedParent as IStyleable;
 
             if (templatedParent == null)
             {
-                throw new InvalidOperationException(
-                    "Cannot call Template selector on control with null TemplatedParent.");
+                return SelectorMatch.NeverThisInstance;
             }
 
             return _parent.Match(templatedParent, subscribe);

+ 16 - 93
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
     /// </summary>
     internal class TypeNameAndClassSelector : Selector
     {
-        private readonly Selector _previous;
+        private readonly Selector? _previous;
         private readonly Lazy<List<string>> _classes = new Lazy<List<string>>(() => new List<string>());
-        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
         /// <summary>
         /// Gets the name of the control to match.
         /// </summary>
-        public string Name { get; set; }
+        public string? Name { get; set; }
 
         /// <inheritdoc/>
-        public override Type TargetType => _targetType ?? _previous?.TargetType;
+        public override Type? TargetType => _targetType ?? _previous?.TargetType;
 
         /// <inheritdoc/>
         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<string> classes, IList<string> 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<bool>
-        {
-            private readonly IList<string> _match;
-            private readonly IAvaloniaReadOnlyList<string> _classes;
-            private bool _hasMatch;
-
-            public ClassObserver(IAvaloniaReadOnlyList<string> classes, IList<string> 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<bool> 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);
-            }
-        }
     }
 }

+ 27 - 8
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -1,6 +1,10 @@
 <Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
         xmlns:sys="clr-namespace:System;assembly=netstandard">
-  
+  <Styles.Resources>
+    <conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter"/>
+  </Styles.Resources>
   <Style Selector="MenuItem">
     <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderThickness" Value="1"/>
@@ -11,7 +15,14 @@
                 Background="{TemplateBinding Background}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}">
-          <Grid ColumnDefinitions="20,5,*,20">
+          <Grid>
+            <Grid.ColumnDefinitions>
+              <ColumnDefinition Width="20"/>
+              <ColumnDefinition Width="5"/>
+              <ColumnDefinition Width="*"/>
+              <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGT"/>
+              <ColumnDefinition Width="20"/>
+            </Grid.ColumnDefinitions>
             <ContentPresenter Name="icon"
                               Content="{TemplateBinding Icon}"
                               Width="16"
@@ -36,12 +47,16 @@
                 </DataTemplate>
               </ContentPresenter.DataTemplates>
             </ContentPresenter>
+            <TextBlock x:Name="PART_InputGestureText"
+                       Grid.Column="3"
+                       Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
+                       VerticalAlignment="Center"/>
             <Path Name="rightArrow"
                   Data="M0,0L4,3.5 0,7z"
                   Fill="{DynamicResource ThemeForegroundBrush}"
                   Margin="10,0,0,0"
                   VerticalAlignment="Center"
-                  Grid.Column="3"/>
+                  Grid.Column="4"/>
             <Popup Name="PART_Popup"
                    PlacementMode="Right"
                    StaysOpen="True"
@@ -54,7 +69,7 @@
                                     Items="{TemplateBinding Items}"
                                     ItemsPanel="{TemplateBinding ItemsPanel}"
                                     ItemTemplate="{TemplateBinding ItemTemplate}"
-                                    Margin="4 2"/>                                      
+                                    Grid.IsSharedSizeScope="True"/>
                 </ScrollViewer>
               </Border>
             </Popup>
@@ -100,10 +115,10 @@
                       BorderThickness="{TemplateBinding BorderThickness}">
                 <ScrollViewer>
                   <ItemsPresenter Name="PART_ItemsPresenter"
-                                    Items="{TemplateBinding Items}"
-                                    ItemsPanel="{TemplateBinding ItemsPanel}"
-                                    ItemTemplate="{TemplateBinding ItemTemplate}"
-                                    Margin="2"/>
+                                  Items="{TemplateBinding Items}"
+                                  ItemsPanel="{TemplateBinding ItemsPanel}"
+                                  ItemTemplate="{TemplateBinding ItemTemplate}"
+                                  Grid.IsSharedSizeScope="True"/>
                 </ScrollViewer>
               </Border>
             </Popup>
@@ -113,6 +128,10 @@
     </Setter>
   </Style>
 
+  <Style Selector="MenuItem /template/ ItemsPresenter#PART_ItemsPresenter">
+    <Setter Property="Margin" Value="2"/>
+  </Style>
+
   <Style Selector="MenuItem:selected /template/ Border#root">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

+ 1 - 0
src/Avalonia.Themes.Default/NativeMenuBar.xaml

@@ -13,6 +13,7 @@
         <Menu.Styles>
           <Style Selector="MenuItem">
             <Setter Property="Header" Value="{Binding Header}"/>
+            <Setter Property="InputGesture" Value="{Binding Gesture}"/>
             <Setter Property="Items" Value="{Binding Menu.Items}"/>
             <Setter Property="Command" Value="{Binding Command}"/>
             <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>

+ 1 - 1
src/Avalonia.Themes.Default/TabControl.xaml

@@ -57,6 +57,6 @@
         <Setter Property="Orientation" Value="Vertical"/>
     </Style>
     <Style Selector="TabControl[TabStripPlacement=Right]">
-        <Setter Property="Padding" Value="0 0 0 4"/>
+        <Setter Property="Padding" Value="0 0 4 0"/>
     </Style>
 </Styles>

+ 27 - 0
src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs

@@ -0,0 +1,27 @@
+using Avalonia.Media;
+
+#nullable enable
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Extensions for <see cref="AvaloniaProperty"/>.
+    /// </summary>
+    public static class AvaloniaPropertyExtensions
+    {
+        /// <summary>
+        /// Checks if values of given property can affect rendering (via <see cref="IAffectsRender"/>).
+        /// </summary>
+        /// <param name="property">Property to check.</param>
+        public static bool CanValueAffectRender(this AvaloniaProperty property)
+        {
+            var propertyType = property.PropertyType;
+
+            // Only case that we are sure that property value CAN'T affect render are sealed types that don't implement
+            // the interface.
+            var cannotAffectRender = propertyType.IsSealed && !typeof(IAffectsRender).IsAssignableFrom(propertyType);
+
+            return !cannotAffectRender;
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Visuals/Media/Brush.cs

@@ -69,14 +69,14 @@ namespace Avalonia.Media
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : Brush
         {
-            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
             {
                 (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty);
             }
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(Invalidate);
+                property.Changed.Subscribe(e => Invalidate(e));
             }
         }
 

+ 17 - 3
src/Avalonia.Visuals/Media/Pen.cs

@@ -2,7 +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 Avalonia.Media.Immutable;
 using Avalonia.Utilities;
 
@@ -197,7 +196,15 @@ namespace Avalonia.Media
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : Pen
         {
-            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Sender is T sender)
+                {
+                    sender.RaiseInvalidated(EventArgs.Empty);
+                }
+            }
+
+            static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e)
             {
                 if (e.Sender is T sender)
                 {
@@ -223,7 +230,14 @@ namespace Avalonia.Media
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(Invalidate);
+                if (property.CanValueAffectRender())
+                {
+                    property.Changed.Subscribe(e => InvalidateAndSubscribe(e));
+                }
+                else
+                {
+                    property.Changed.Subscribe(e => Invalidate(e));
+                }
             }
         }
 

+ 18 - 9
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting
             var availableWidth = paragraphWidth;
             var currentWidth = 0.0;
             var runIndex = 0;
+            var length = 0;
 
             while (runIndex < textRuns.Count)
             {
                 var currentRun = textRuns[runIndex];
 
-                currentWidth += currentRun.GlyphRun.Bounds.Width;
-
-                if (currentWidth > availableWidth)
+                if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
                 {
-                    var measuredLength = MeasureText(currentRun, paragraphWidth);
+                    var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
 
-                    if (measuredLength < text.End)
+                    if (measuredLength < currentRun.Text.Length)
                     {
                         var currentBreakPosition = -1;
 
@@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    var splitResult = SplitTextRuns(textRuns, measuredLength);
+                    length += measuredLength;
+
+                    var splitResult = SplitTextRuns(textRuns, length);
 
                     var textLineMetrics =
                         TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
 
-                    return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
+                    return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics);
                 }
 
-                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                length += currentRun.GlyphRun.Characters.Length;
 
                 runIndex++;
             }
@@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting
 
                 if (measuredWidth + advance > availableWidth)
                 {
+                    index--;
                     break;
                 }
 
                 measuredWidth += advance;
             }
 
+            if(index < 0)
+            {
+                return 0;
+            }
+
             var cluster = textRun.GlyphRun.GlyphClusters[index];
 
             var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
@@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
+                var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
 
                 var first = new ShapedTextRun[firstCount];
 

+ 11 - 5
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -233,6 +233,12 @@ namespace Avalonia.Media.TextFormatting
 
                         var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties);
 
+                        if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            currentPosition = _text.Length;
+                            break;
+                        }
+
                         UpdateBounds(textLine, ref left, ref right, ref bottom);
 
                         textLines.Add(textLine);
@@ -253,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
                     {
                         var emptyTextLine = CreateEmptyTextLine(currentPosition);
 
+                        if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            break;
+                        }
+
                         UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
 
                         textLines.Add(emptyTextLine);
 
                         break;
                     }
-
-                    if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height)
-                    {
-                        break;
-                    }
                 }
 
                 Bounds = new Rect(left, 0, right, bottom);

+ 18 - 4
src/Avalonia.Visuals/Visual.cs

@@ -3,7 +3,6 @@
 
 using System;
 using System.Collections.Specialized;
-using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Data;
 using Avalonia.Logging;
@@ -336,7 +335,15 @@ namespace Avalonia
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : Visual
         {
-            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Sender is T sender)
+                {
+                    sender.InvalidateVisual();
+                }
+            }
+
+            static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e)
             {
                 if (e.Sender is T sender)
                 {
@@ -347,7 +354,7 @@ namespace Avalonia
 
                     if (e.NewValue is IAffectsRender newValue)
                     {
-                        WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated);                        
+                        WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated);
                     }
 
                     sender.InvalidateVisual();
@@ -356,7 +363,14 @@ namespace Avalonia
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(Invalidate);
+                if (property.CanValueAffectRender())
+                {
+                    property.Changed.Subscribe(e => InvalidateAndSubscribe(e));
+                }
+                else
+                {
+                    property.Changed.Subscribe(e => Invalidate(e));
+                }
             }
         }
 

+ 2 - 17
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;
 
         /// <inheritdoc/>
-        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);
 
         /// <inheritdoc/>
         public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);

Some files were not shown because too many files changed in this diff