Browse Source

Merge branch 'master' into feature/menuitem-inputgesturetext

pull/3602/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
980b4dba87
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      azure-pipelines.yml
  2. 3
      global.json
  3. 166
      nukebuild/Build.cs
  4. 21
      nukebuild/BuildParameters.cs
  5. 8
      nukebuild/Shims.cs
  6. 8
      nukebuild/_build.csproj
  7. 6
      packages/Avalonia/Avalonia.csproj
  8. 2
      packages/Avalonia/Avalonia.props
  9. 2
      samples/BindingDemo/BindingDemo.csproj
  10. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  11. 2
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  12. 2
      samples/Previewer/Previewer.csproj
  13. 2
      samples/RemoteDemo/RemoteDemo.csproj
  14. 2
      samples/RenderDemo/RenderDemo.csproj
  15. 2
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  16. 9
      src/Avalonia.Base/AvaloniaObject.cs
  17. 17
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  18. 14
      src/Avalonia.Base/AvaloniaProperty.cs
  19. 11
      src/Avalonia.Base/DirectPropertyBase.cs
  20. 2
      src/Avalonia.Base/IAvaloniaObject.cs
  21. 11
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  22. 9
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  23. 2
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  24. 13
      src/Avalonia.Base/StyledPropertyBase.cs
  25. 34
      src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs
  26. 25
      src/Avalonia.Base/ValueStore.cs
  27. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  28. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  29. 1
      src/Avalonia.Styling/Avalonia.Styling.csproj
  30. 6
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  31. 117
      src/Avalonia.Styling/StyledElement.cs
  32. 77
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  33. 110
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  34. 133
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  35. 71
      src/Avalonia.Styling/Styling/Activators/AndActivator.cs
  36. 43
      src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs
  37. 33
      src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs
  38. 17
      src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs
  39. 16
      src/Avalonia.Styling/Styling/Activators/NotActivator.cs
  40. 71
      src/Avalonia.Styling/Styling/Activators/OrActivator.cs
  41. 45
      src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs
  42. 38
      src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
  43. 58
      src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs
  44. 76
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  45. 25
      src/Avalonia.Styling/Styling/DescendentSelector.cs
  46. 18
      src/Avalonia.Styling/Styling/ISetter.cs
  47. 40
      src/Avalonia.Styling/Styling/ISetterInstance.cs
  48. 16
      src/Avalonia.Styling/Styling/IStyle.cs
  49. 22
      src/Avalonia.Styling/Styling/IStyleInstance.cs
  50. 20
      src/Avalonia.Styling/Styling/IStyleable.cs
  51. 18
      src/Avalonia.Styling/Styling/NotSelector.cs
  52. 38
      src/Avalonia.Styling/Styling/OrSelector.cs
  53. 25
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  54. 180
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  55. 118
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  56. 129
      src/Avalonia.Styling/Styling/Selector.cs
  57. 58
      src/Avalonia.Styling/Styling/SelectorMatch.cs
  58. 146
      src/Avalonia.Styling/Styling/Setter.cs
  59. 168
      src/Avalonia.Styling/Styling/Style.cs
  60. 56
      src/Avalonia.Styling/Styling/StyleActivator.cs
  61. 135
      src/Avalonia.Styling/Styling/StyleInstance.cs
  62. 25
      src/Avalonia.Styling/Styling/Styler.cs
  63. 76
      src/Avalonia.Styling/Styling/Styles.cs
  64. 5
      src/Avalonia.Styling/Styling/TemplateSelector.cs
  65. 109
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  66. 19
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  67. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  68. 2
      tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj
  69. 2
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  70. 35
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  71. 8
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  72. 6
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  73. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  74. 2
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  75. 6
      tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs
  76. 2
      tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj
  77. 2
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  78. 8
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  79. 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  80. 2
      tests/Avalonia.Controls.UnitTests/UserControlTests.cs
  81. 2
      tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj
  82. 2
      tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj
  83. 6
      tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs
  84. 2
      tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj
  85. 2
      tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj
  86. 2
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  87. 3
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  88. 2
      tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj
  89. 2
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  90. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  91. 10
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  92. 37
      tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
  93. 2
      tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj
  94. 2
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  95. 2
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  96. 71
      tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs
  97. 92
      tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs
  98. 75
      tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs
  99. 2
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  100. 16
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

14
azure-pipelines.yml

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

3
global.json

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

166
nukebuild/Build.cs

@ -13,6 +13,7 @@ using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild; using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Utilities; using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; 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 running and debugging a particular target (optionally without deps) would be way easier
ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support
VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support
*/ */
partial class Build : NukeBuild partial class Build : NukeBuild
{ {
[Solution("Avalonia.sln")] readonly Solution Solution;
static Lazy<string> MsBuildExe = new Lazy<string>(() => static Lazy<string> MsBuildExe = new Lazy<string>(() =>
{ {
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -54,7 +57,7 @@ partial class Build : NukeBuild
protected override void OnBuildInitialized() protected override void OnBuildInitialized()
{ {
Parameters = new BuildParameters(this); 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.Version,
Parameters.Configuration, Parameters.Configuration,
typeof(NukeBuild).Assembly.GetName().Version.ToString()); typeof(NukeBuild).Assembly.GetName().Version.ToString());
@ -93,29 +96,24 @@ partial class Build : NukeBuild
string projectFile, string projectFile,
Configure<MSBuildSettings> configurator = null) Configure<MSBuildSettings> configurator = null)
{ {
return MSBuild(projectFile, c => return MSBuild(c => c
{ .SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines // This is required for VS2019 image on Azure Pipelines
if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) .When(Parameters.IsRunningOnWindows &&
{ Parameters.IsRunningOnAzure, c => c
var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); .AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_8_X64")))
if (javaSdk != null) .AddProperty("PackageVersion", Parameters.Version)
c = c.AddProperty("JavaSdkDirectory", javaSdk); .AddProperty("iOSRoslynPathHackRequired", true)
} .SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
c = c.AddProperty("PackageVersion", Parameters.Version) .SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("iOSRoslynPathHackRequired", "true") .Apply(configurator));
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal);
c = configurator?.Invoke(c) ?? c;
return c;
});
} }
Target Clean => _ => _.Executes(() => Target Clean => _ => _.Executes(() =>
{ {
DeleteDirectories(Parameters.BuildDirs); Parameters.BuildDirs.ForEach(DeleteDirectory);
EnsureCleanDirectories(Parameters.BuildDirs); Parameters.BuildDirs.ForEach(EnsureCleanDirectory);
EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.ArtifactsDir);
EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetIntermediateRoot);
EnsureCleanDirectory(Parameters.NugetRoot); EnsureCleanDirectory(Parameters.NugetRoot);
@ -134,97 +132,84 @@ partial class Build : NukeBuild
); );
else else
DotNetBuild(Parameters.MSBuildSolution, c => c DotNetBuild(c => c
.SetProjectFile(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version) .AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration) .SetConfiguration(Parameters.Configuration)
); );
}); });
void RunCoreTest(string project) void RunCoreTest(string projectName)
{ {
if(!project.EndsWith(".csproj")) Information($"Running tests from {projectName}");
project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); var project = Solution.GetProject(projectName).NotNull("project != null");
Information("Running tests from " + project);
XDocument xdoc; foreach (var fw in project.GetTargetFrameworks())
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)
{ {
if (fw.StartsWith("net4") if (fw.StartsWith("net4")
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
&& Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") && 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; continue;
} }
Information("Running for " + fw); Information($"Running for {projectName} ({fw}) ...");
DotNetTest(c =>
{ DotNetTest(c => c
c = c .SetProjectFile(project)
.SetProjectFile(project) .SetConfiguration(Parameters.Configuration)
.SetConfiguration(Parameters.Configuration) .SetFramework(fw)
.SetFramework(fw) .EnableNoBuild()
.EnableNoBuild() .EnableNoRestore()
.EnableNoRestore(); .When(Parameters.PublishTestResults, c => c
// NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient .SetLogger("trx")
if (Parameters.PublishTestResults) .SetResultsDirectory(Parameters.TestResultsRoot)));
c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot);
return c;
});
} }
} }
Target RunCoreLibsTests => _ => _ Target RunCoreLibsTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests) .OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.Animation.UnitTests"); RunCoreTest("Avalonia.Animation.UnitTests");
RunCoreTest("./tests/Avalonia.Base.UnitTests"); RunCoreTest("Avalonia.Base.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.UnitTests"); RunCoreTest("Avalonia.Controls.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); RunCoreTest("Avalonia.Controls.DataGrid.UnitTests");
RunCoreTest("./tests/Avalonia.Input.UnitTests"); RunCoreTest("Avalonia.Input.UnitTests");
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); RunCoreTest("Avalonia.Interactivity.UnitTests");
RunCoreTest("./tests/Avalonia.Layout.UnitTests"); RunCoreTest("Avalonia.Layout.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.UnitTests"); RunCoreTest("Avalonia.Markup.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("./tests/Avalonia.Styling.UnitTests"); RunCoreTest("Avalonia.Styling.UnitTests");
RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("./tests/Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests) .OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); RunCoreTest("Avalonia.Skia.RenderTests");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); RunCoreTest("Avalonia.Direct2D1.RenderTests");
}); });
Target RunDesignerTests => _ => _ Target RunDesignerTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); RunCoreTest("Avalonia.DesignerSupport.Tests");
}); });
[PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit;
Target RunLeakTests => _ => _ Target RunLeakTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
@ -235,7 +220,7 @@ partial class Build : NukeBuild
}); });
Target ZipFiles => _ => _ Target ZipFiles => _ => _
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
.Executes(() => .Executes(() =>
{ {
var data = Parameters; var data = Parameters;
@ -259,9 +244,10 @@ partial class Build : NukeBuild
MsBuildCommon(Parameters.MSBuildSolution, c => c MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack")); .AddTargets("Pack"));
else else
DotNetPack(Parameters.MSBuildSolution, c => DotNetPack(c => c
c.SetConfiguration(Parameters.Configuration) .SetProject(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version)); .SetConfiguration(Parameters.Configuration)
.AddProperty("PackageVersion", Parameters.Version));
}); });
Target CreateNugetPackages => _ => _ Target CreateNugetPackages => _ => _
@ -274,32 +260,40 @@ partial class Build : NukeBuild
new NumergeNukeLogger())) new NumergeNukeLogger()))
throw new Exception("Package merge failed"); throw new Exception("Package merge failed");
}); });
Target RunTests => _ => _ Target RunTests => _ => _
.DependsOn(RunCoreLibsTests) .DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests) .DependsOn(RunRenderTests)
.DependsOn(RunDesignerTests) .DependsOn(RunDesignerTests)
.DependsOn(RunLeakTests); .DependsOn(RunLeakTests);
Target Package => _ => _ Target Package => _ => _
.DependsOn(RunTests) .DependsOn(RunTests)
.DependsOn(CreateNugetPackages); .DependsOn(CreateNugetPackages);
Target CiAzureLinux => _ => _ Target CiAzureLinux => _ => _
.DependsOn(RunTests); .DependsOn(RunTests);
Target CiAzureOSX => _ => _ Target CiAzureOSX => _ => _
.DependsOn(Package) .DependsOn(Package)
.DependsOn(ZipFiles); .DependsOn(ZipFiles);
Target CiAzureWindows => _ => _ Target CiAzureWindows => _ => _
.DependsOn(Package) .DependsOn(Package)
.DependsOn(ZipFiles); .DependsOn(ZipFiles);
public static int Main() => public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Execute<Build>(x => x.Package) ? Execute<Build>(x => x.Package)
: Execute<Build>(x => x.RunTests); : 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;
}
}

21
nukebuild/BuildParameters.cs

@ -4,24 +4,21 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Xml.Linq; using System.Xml.Linq;
using Nuke.Common; using Nuke.Common;
using Nuke.Common.BuildServers; using Nuke.Common.CI.AzurePipelines;
using Nuke.Common.Execution;
using Nuke.Common.IO; using Nuke.Common.IO;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
public partial class Build public partial class Build
{ {
[Parameter("configuration")] [Parameter("configuration")]
public string Configuration { get; set; } public string Configuration { get; set; }
[Parameter("skip-tests")] [Parameter("skip-tests")]
public bool SkipTests { get; set; } public bool SkipTests { get; set; }
[Parameter("force-nuget-version")] [Parameter("force-nuget-version")]
public string ForceNugetVersion { get; set; } public string ForceNugetVersion { get; set; }
public class BuildParameters public class BuildParameters
{ {
public string Configuration { get; } public string Configuration { get; }
@ -79,15 +76,15 @@ public partial class Build
IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX; Environment.OSVersion.Platform == PlatformID.MacOSX;
IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsRunningOnAzure = Host == HostType.TeamServices || IsRunningOnAzure = Host == HostType.AzurePipelines ||
Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; Environment.GetEnvironmentVariable("LOGNAME") == "vsts";
if (IsRunningOnAzure) if (IsRunningOnAzure)
{ {
RepositoryName = TeamServices.Instance.RepositoryUri; RepositoryName = AzurePipelines.Instance.RepositoryUri;
RepositoryBranch = TeamServices.Instance.SourceBranch; RepositoryBranch = AzurePipelines.Instance.SourceBranch;
IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue;
IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri);
} }
IsMainRepo = IsMainRepo =
StringComparer.OrdinalIgnoreCase.Equals(MainRepo, StringComparer.OrdinalIgnoreCase.Equals(MainRepo,

8
nukebuild/Shims.cs

@ -19,9 +19,9 @@ public partial class Build
Logger.Info(info, args); 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(); var targetPath = target.ToString();
bool finished = false, atLeastOneFileAdded = false; bool finished = false, atLeastOneFileAdded = false;
@ -38,7 +38,7 @@ public partial class Build
fileStream.CopyTo(entryStream); fileStream.CopyTo(entryStream);
atLeastOneFileAdded = true; atLeastOneFileAdded = true;
} }
foreach (var path in paths) foreach (var path in paths)
{ {
if (Directory.Exists(path)) if (Directory.Exists(path))
@ -64,7 +64,7 @@ public partial class Build
finished = true; finished = true;
} }
finally finally
{ {
try try
{ {

8
nukebuild/_build.csproj

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

6
packages/Avalonia/Avalonia.csproj

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

2
packages/Avalonia/Avalonia.props

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

2
samples/BindingDemo/BindingDemo.csproj

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

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

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

2
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

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

2
samples/Previewer/Previewer.csproj

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

2
samples/RemoteDemo/RemoteDemo.csproj

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

2
samples/RenderDemo/RenderDemo.csproj

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

2
samples/VirtualizationDemo/VirtualizationDemo.csproj

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

9
src/Avalonia.Base/AvaloniaObject.cs

@ -311,7 +311,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of 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, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
@ -335,8 +338,10 @@ namespace Avalonia
} }
else if (!(value is DoNothingType)) else if (!(value is DoNothingType))
{ {
Values.SetValue(property, value, priority); return Values.SetValue(property, value, priority);
} }
return null;
} }
/// <summary> /// <summary>

17
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -458,7 +458,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of 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, this IAvaloniaObject target,
AvaloniaProperty property, AvaloniaProperty property,
object value, object value,
@ -467,7 +470,7 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
property.RouteSetValue(target, value, priority); return property.RouteSetValue(target, value, priority);
} }
/// <summary> /// <summary>
@ -478,7 +481,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of 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, this IAvaloniaObject target,
AvaloniaProperty<T> property, AvaloniaProperty<T> property,
T value, T value,
@ -490,11 +496,10 @@ namespace Avalonia
switch (property) switch (property)
{ {
case StyledPropertyBase<T> styled: case StyledPropertyBase<T> styled:
target.SetValue(styled, value, priority); return target.SetValue(styled, value, priority);
break;
case DirectPropertyBase<T> direct: case DirectPropertyBase<T> direct:
target.SetValue(direct, value); target.SetValue(direct, value);
break; return null;
default: default:
throw new NotSupportedException("Unsupported AvaloniaProperty type."); throw new NotSupportedException("Unsupported AvaloniaProperty type.");
} }

14
src/Avalonia.Base/AvaloniaProperty.cs

@ -469,6 +469,15 @@ namespace Avalonia
return Name; 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> /// <summary>
/// Notifies the <see cref="Changed"/> observable. /// Notifies the <see cref="Changed"/> observable.
/// </summary> /// </summary>
@ -496,7 +505,10 @@ namespace Avalonia
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority.</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, IAvaloniaObject o,
object value, object value,
BindingPriority priority); BindingPriority priority);

11
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities;
#nullable enable #nullable enable
@ -101,6 +102,12 @@ namespace Avalonia
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type); return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
} }
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{
vistor.Visit(this, ref data);
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o) internal override void RouteClearValue(IAvaloniaObject o)
{ {
@ -114,7 +121,7 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteSetValue( internal override IDisposable? RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority) BindingPriority priority)
@ -133,6 +140,8 @@ namespace Avalonia
{ {
throw v.Error!; throw v.Error!;
} }
return null;
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/Avalonia.Base/IAvaloniaObject.cs

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

11
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore
/// <see cref="PriorityValue{T}"/>. /// <see cref="PriorityValue{T}"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam> /// <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( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority) BindingPriority priority,
IValueSink sink)
{ {
Property = property; Property = property;
Value = value; Value = value;
Priority = priority; Priority = priority;
_sink = sink;
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore
Optional<object> IValue.Value => Value.ToObject(); Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority; BindingPriority IValue.ValuePriority => Priority;
public void Reparent(IValueSink sink) { } public void Dispose() => _sink.Completed(Property, this, Value);
public void Reparent(IValueSink sink) => _sink = sink;
} }
} }

9
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -78,8 +78,10 @@ namespace Avalonia.PropertyStore
public void ClearLocalValue() => UpdateEffectiveValue(); 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) if (priority == BindingPriority.LocalValue)
{ {
_localValue = value; _localValue = value;
@ -87,10 +89,13 @@ namespace Avalonia.PropertyStore
else else
{ {
var insert = FindInsertPoint(priority); 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(); UpdateEffectiveValue();
return result;
} }
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority) public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)

2
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

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

13
src/Avalonia.Base/StyledPropertyBase.cs

@ -5,6 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -169,6 +170,12 @@ namespace Avalonia
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
} }
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{
vistor.Visit(this, ref data);
}
/// <summary> /// <summary>
/// Gets the string representation of the property. /// Gets the string representation of the property.
/// </summary> /// </summary>
@ -194,7 +201,7 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteSetValue( internal override IDisposable RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority) BindingPriority priority)
@ -203,7 +210,7 @@ namespace Avalonia
if (v.HasValue) 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) else if (v.Type == BindingValueType.UnsetValue)
{ {
@ -213,6 +220,8 @@ namespace Avalonia
{ {
throw v.Error; throw v.Error;
} }
return null;
} }
/// <inheritdoc/> /// <inheritdoc/>

34
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);
}
}

25
src/Avalonia.Base/ValueStore.cs

@ -70,23 +70,25 @@ namespace Avalonia
return false; 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) if (property.ValidateValue?.Invoke(value) == false)
{ {
throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
} }
IDisposable? result = null;
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
SetExisting(slot, property, value, priority); result = SetExisting(slot, property, value, priority);
} }
else if (property.HasCoercion) else if (property.HasCoercion)
{ {
// If the property has any coercion callbacks then always create a PriorityValue. // If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this); var entry = new PriorityValue<T>(_owner, property, this);
_values.AddValue(property, entry); _values.AddValue(property, entry);
entry.SetValue(value, priority); result = entry.SetValue(value, priority);
} }
else if (priority == BindingPriority.LocalValue) else if (priority == BindingPriority.LocalValue)
{ {
@ -95,10 +97,13 @@ namespace Avalonia
} }
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority); var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry); _values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value); _sink.ValueChanged(property, priority, default, value);
result = entry;
} }
return result;
} }
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
@ -205,21 +210,23 @@ namespace Avalonia
} }
} }
private void SetExisting<T>( private IDisposable? SetExisting<T>(
object slot, object slot,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority) BindingPriority priority)
{ {
IDisposable? result = null;
if (slot is IPriorityValueEntry<T> e) if (slot is IPriorityValueEntry<T> e)
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, e); var priorityValue = new PriorityValue<T>(_owner, property, this, e);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
priorityValue.SetValue(value, priority); result = priorityValue.SetValue(value, priority);
} }
else if (slot is PriorityValue<T> p) else if (slot is PriorityValue<T> p)
{ {
p.SetValue(value, priority); result = p.SetValue(value, priority);
} }
else if (slot is LocalValueEntry<T> l) else if (slot is LocalValueEntry<T> l)
{ {
@ -232,7 +239,7 @@ namespace Avalonia
else else
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, l); var priorityValue = new PriorityValue<T>(_owner, property, this, l);
priorityValue.SetValue(value, priority); result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
} }
} }
@ -240,6 +247,8 @@ namespace Avalonia
{ {
throw new NotSupportedException("Unrecognised value store slot type."); throw new NotSupportedException("Unrecognised value store slot type.");
} }
return result;
} }
private IDisposable BindExisting<T>( private IDisposable BindExisting<T>(

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

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

17
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -6,6 +6,8 @@ using System.Collections.Specialized;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels
Type = visual.GetType().Name; Type = visual.GetType().Name;
Visual = visual; 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< var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>( NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x, x => control.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x) x => control.Classes.CollectionChanged -= x)
.TakeUntil(((IStyleable)styleable).StyleDetach); .TakeUntil(removed);
classesChanged.Select(_ => Unit.Default) classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default) .StartWith(Unit.Default)
.Subscribe(_ => .Subscribe(_ =>
{ {
if (styleable.Classes.Count > 0) if (control.Classes.Count > 0)
{ {
Classes = "(" + string.Join(" ", styleable.Classes) + ")"; Classes = "(" + string.Join(" ", control.Classes) + ")";
} }
else else
{ {

1
src/Avalonia.Styling/Avalonia.Styling.csproj

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

6
src/Avalonia.Styling/Controls/NameScopeLocator.cs

@ -1,11 +1,5 @@
using System; using System;
using System.Linq;
using System.Reactive.Disposables; 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; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls

117
src/Avalonia.Styling/StyledElement.cs

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

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);
}
}
}

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);
}
}
}

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
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
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
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
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
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
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
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
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
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
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);
}
}

25
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
internal class DescendantSelector : Selector internal class DescendantSelector : Selector
{ {
private readonly Selector _parent; private readonly Selector _parent;
private string _selectorString; private string? _selectorString;
public DescendantSelector(Selector parent) public DescendantSelector(Selector? parent)
{ {
if (parent == null) _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
{
throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
}
_parent = parent;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -29,7 +26,7 @@ namespace Avalonia.Styling
public override bool InTemplate => _parent.InTemplate; public override bool InTemplate => _parent.InTemplate;
/// <inheritdoc/> /// <inheritdoc/>
public override Type TargetType => null; public override Type? TargetType => null;
public override string ToString() public override string ToString()
{ {
@ -43,8 +40,8 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{ {
ILogical c = (ILogical)control; var c = (ILogical)control;
List<IObservable<bool>> descendantMatches = new List<IObservable<bool>>(); var descendantMatches = new OrActivatorBuilder();
while (c != null) while (c != null)
{ {
@ -67,7 +64,7 @@ namespace Avalonia.Styling
if (descendantMatches.Count > 0) if (descendantMatches.Count > 0)
{ {
return new SelectorMatch(StyleActivator.Or(descendantMatches)); return new SelectorMatch(descendantMatches.Get());
} }
else else
{ {
@ -75,6 +72,6 @@ namespace Avalonia.Styling
} }
} }
protected override Selector MovePrevious() => null; protected override Selector? MovePrevious() => null;
} }
} }

18
src/Avalonia.Styling/Styling/ISetter.cs

@ -3,6 +3,8 @@
using System; using System;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -11,11 +13,15 @@ namespace Avalonia.Styling
public interface ISetter public interface ISetter
{ {
/// <summary> /// <summary>
/// Applies the setter to a control. /// Instances a setter on a control.
/// </summary> /// </summary>
/// <param name="style">The style that is being applied.</param> /// <param name="target">The control.</param>
/// <param name="control">The control.</param> /// <returns>An <see cref="ISetterInstance"/>.</returns>
/// <param name="activator">An optional activator.</param> /// <remarks>
IDisposable Apply(IStyle style, IStyleable control, IObservable<bool> activator); /// 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
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();
}
}

16
src/Avalonia.Styling/Styling/IStyle.cs

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

22
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();
}
}

20
src/Avalonia.Styling/Styling/IStyleable.cs

@ -4,6 +4,8 @@
using System; using System;
using Avalonia.Collections; using Avalonia.Collections;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -11,11 +13,6 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
public interface IStyleable : IAvaloniaObject, INamed public interface IStyleable : IAvaloniaObject, INamed
{ {
/// <summary>
/// Signaled when the control's style should be removed.
/// </summary>
IObservable<IStyleable> StyleDetach { get; }
/// <summary> /// <summary>
/// Gets the list of classes for the control. /// Gets the list of classes for the control.
/// </summary> /// </summary>
@ -29,6 +26,17 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets the template parent of this element if the control comes from a template. /// Gets the template parent of this element if the control comes from a template.
/// </summary> /// </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();
} }
} }

18
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Linq; using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -11,16 +13,16 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
internal class NotSelector : Selector internal class NotSelector : Selector
{ {
private readonly Selector _previous; private readonly Selector? _previous;
private readonly Selector _argument; private readonly Selector _argument;
private string _selectorString; private string? _selectorString;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NotSelector"/> class. /// Initializes a new instance of the <see cref="NotSelector"/> class.
/// </summary> /// </summary>
/// <param name="previous">The previous selector.</param> /// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</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; _previous = previous;
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
@ -33,14 +35,14 @@ namespace Avalonia.Styling
public override bool IsCombinator => false; public override bool IsCombinator => false;
/// <inheritdoc/> /// <inheritdoc/>
public override Type TargetType => _previous?.TargetType; public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() public override string ToString()
{ {
if (_selectorString == null) if (_selectorString == null)
{ {
_selectorString = ":not(" + _argument.ToString() + ")"; _selectorString = $"{_previous?.ToString()}:not({_argument})";
} }
return _selectorString; return _selectorString;
@ -61,12 +63,12 @@ namespace Avalonia.Styling
case SelectorMatchResult.NeverThisType: case SelectorMatchResult.NeverThisType:
return SelectorMatch.AlwaysThisType; return SelectorMatch.AlwaysThisType;
case SelectorMatchResult.Sometimes: case SelectorMatchResult.Sometimes:
return new SelectorMatch(innerResult.Activator.Select(x => !x)); return new SelectorMatch(new NotActivator(innerResult.Activator!));
default: default:
throw new InvalidOperationException("Invalid SelectorMatchResult."); throw new InvalidOperationException("Invalid SelectorMatchResult.");
} }
} }
protected override Selector MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
} }
} }

38
src/Avalonia.Styling/Styling/OrSelector.cs

@ -3,6 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -12,8 +15,8 @@ namespace Avalonia.Styling
internal class OrSelector : Selector internal class OrSelector : Selector
{ {
private readonly IReadOnlyList<Selector> _selectors; private readonly IReadOnlyList<Selector> _selectors;
private string _selectorString; private string? _selectorString;
private Type _targetType; private Type? _targetType;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OrSelector"/> class. /// 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> /// <param name="selectors">The selectors to OR.</param>
public OrSelector(IReadOnlyList<Selector> selectors) public OrSelector(IReadOnlyList<Selector> selectors)
{ {
Contract.Requires<ArgumentNullException>(selectors != null); if (selectors is null)
Contract.Requires<ArgumentException>(selectors.Count > 1); {
throw new ArgumentNullException(nameof(selectors));
}
if (selectors.Count <= 1)
{
throw new ArgumentException("Need more than one selector to OR.");
}
_selectors = selectors; _selectors = selectors;
} }
@ -34,7 +44,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false; public override bool IsCombinator => false;
/// <inheritdoc/> /// <inheritdoc/>
public override Type TargetType public override Type? TargetType
{ {
get get
{ {
@ -60,7 +70,7 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{ {
var activators = new List<IObservable<bool>>(); var activators = new OrActivatorBuilder();
var neverThisInstance = false; var neverThisInstance = false;
foreach (var selector in _selectors) foreach (var selector in _selectors)
@ -76,18 +86,14 @@ namespace Avalonia.Styling
neverThisInstance = true; neverThisInstance = true;
break; break;
case SelectorMatchResult.Sometimes: case SelectorMatchResult.Sometimes:
activators.Add(match.Activator); activators.Add(match.Activator!);
break; break;
} }
} }
if (activators.Count > 1) if (activators.Count > 0)
{
return new SelectorMatch(StyleActivator.Or(activators));
}
else if (activators.Count == 1)
{ {
return new SelectorMatch(activators[0]); return new SelectorMatch(activators.Get());
} }
else if (neverThisInstance) 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) foreach (var selector in _selectors)
{ {

25
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Linq;
using System.Text; using System.Text;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -13,14 +15,14 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
internal class PropertyEqualsSelector : Selector internal class PropertyEqualsSelector : Selector
{ {
private readonly Selector _previous; private readonly Selector? _previous;
private readonly AvaloniaProperty _property; private readonly AvaloniaProperty _property;
private readonly object _value; private readonly object? _value;
private string _selectorString; 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; _previous = previous;
_property = property; _property = property;
@ -33,13 +35,8 @@ namespace Avalonia.Styling
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsCombinator => false; public override bool IsCombinator => false;
/// <summary>
/// Gets the name of the control to match.
/// </summary>
public string Name { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public override Type TargetType => _previous?.TargetType; public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() public override string ToString()
@ -77,7 +74,7 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value));
} }
else else
{ {
@ -86,6 +83,6 @@ namespace Avalonia.Styling
} }
} }
protected override Selector MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
} }
} }

180
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
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);
}
}
}

129
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic; using Avalonia.Styling.Activators;
using System.Diagnostics;
using Avalonia.Utilities; #nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -30,7 +30,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets the target type of the selector, if available. /// Gets the target type of the selector, if available.
/// </summary> /// </summary>
public abstract Type TargetType { get; } public abstract Type? TargetType { get; }
/// <summary> /// <summary>
/// Tries to match the selector with a control. /// Tries to match the selector with a control.
@ -43,54 +43,28 @@ namespace Avalonia.Styling
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <returns>A <see cref="SelectorMatch"/>.</returns>
public SelectorMatch Match(IStyleable control, bool subscribe = true) public SelectorMatch Match(IStyleable control, bool subscribe = true)
{ {
ValueSingleOrList<IObservable<bool>> inputs = default; // 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
var selector = this; // will be on the left.
var alwaysThisType = true; var match = MatchUntilCombinator(control, this, subscribe, out var combinator);
var hitCombinator = false;
// If the pre-combinator selector matches, we can now match the combinator, if any.
while (selector != null) if (match.IsMatch && combinator is object)
{ {
hitCombinator |= selector.IsCombinator; match = match.And(combinator.Match(control, subscribe));
var match = selector.Evaluate(control, subscribe); // 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
if (!match.IsMatch) // control.
{ match = match.Result switch
return hitCombinator ? SelectorMatch.NeverThisInstance : match;
}
else if (selector.InTemplate && control.TemplatedParent == null)
{ {
return SelectorMatch.NeverThisInstance; SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance,
} SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance,
else if (match.Result == SelectorMatchResult.AlwaysThisInstance) _ => match
{ };
alwaysThisType = false;
}
else if (match.Result == SelectorMatchResult.Sometimes)
{
Debug.Assert(match.Activator != null);
inputs.Add(match.Activator);
}
selector = selector.MovePrevious();
} }
if (inputs.HasList) return match;
{
return new SelectorMatch(StyleActivator.And(inputs.List));
}
else if (inputs.IsSingle)
{
return new SelectorMatch(inputs.Single);
}
else
{
return alwaysThisType && !hitCombinator ?
SelectorMatch.AlwaysThisType :
SelectorMatch.AlwaysThisInstance;
}
} }
/// <summary> /// <summary>
@ -107,6 +81,65 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Moves to the previous selector. /// Moves to the previous selector.
/// </summary> /// </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;
}
} }
} }

58
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -21,9 +24,9 @@ namespace Avalonia.Styling
NeverThisInstance, NeverThisInstance,
/// <summary> /// <summary>
/// The selector always matches this type. /// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>.
/// </summary> /// </summary>
AlwaysThisType, Sometimes,
/// <summary> /// <summary>
/// The selector always matches this instance, but doesn't always match this type. /// The selector always matches this instance, but doesn't always match this type.
@ -31,9 +34,9 @@ namespace Avalonia.Styling
AlwaysThisInstance, AlwaysThisInstance,
/// <summary> /// <summary>
/// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>. /// The selector always matches this type.
/// </summary> /// </summary>
Sometimes, AlwaysThisType,
} }
/// <summary> /// <summary>
@ -43,7 +46,7 @@ namespace Avalonia.Styling
/// A selector match describes whether and how a <see cref="Selector"/> matches a control, and /// 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. /// in addition whether the selector can ever match a control of the same type.
/// </remarks> /// </remarks>
public class SelectorMatch public readonly struct SelectorMatch
{ {
/// <summary> /// <summary>
/// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>. /// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>.
@ -70,20 +73,28 @@ namespace Avalonia.Styling
/// <see cref="SelectorMatchResult.Sometimes"/> result. /// <see cref="SelectorMatchResult.Sometimes"/> result.
/// </summary> /// </summary>
/// <param name="match">The match activator.</param> /// <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; Result = SelectorMatchResult.Sometimes;
Activator = match; 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> /// <summary>
/// Gets a value indicating whether the match was positive. /// Gets a value indicating whether the match was positive.
/// </summary> /// </summary>
public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType; public bool IsMatch => Result >= SelectorMatchResult.Sometimes;
/// <summary> /// <summary>
/// Gets the result of the match. /// Gets the result of the match.
@ -91,9 +102,34 @@ namespace Avalonia.Styling
public SelectorMatchResult Result { get; } public SelectorMatchResult Result { get; }
/// <summary> /// <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. /// change over time.
/// </summary> /// </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();
} }
} }

146
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Disposables;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Reactive; using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Styling 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 /// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
/// <see cref="AvaloniaObject"/> depending on a condition. /// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks> /// </remarks>
public class Setter : ISetter, IAnimationSetter public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor<Setter.SetterVisitorData>
{ {
private object _value; private object? _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Setter"/> class. /// Initializes a new instance of the <see cref="Setter"/> class.
@ -42,11 +43,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets or sets the property to set. /// Gets or sets the property to set.
/// </summary> /// </summary>
public AvaloniaProperty Property public AvaloniaProperty? Property { get; set; }
{
get;
set;
}
/// <summary> /// <summary>
/// Gets or sets the property value. /// Gets or sets the property value.
@ -54,13 +51,9 @@ namespace Avalonia.Styling
[Content] [Content]
[AssignBinding] [AssignBinding]
[DependsOn(nameof(Property))] [DependsOn(nameof(Property))]
public object Value public object? Value
{ {
get get => _value;
{
return _value;
}
set set
{ {
(value as ISetterValue)?.Initialize(this); (value as ISetterValue)?.Initialize(this);
@ -68,99 +61,78 @@ namespace Avalonia.Styling
} }
} }
/// <summary> public ISetterInstance Instance(IStyleable target)
/// 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)
{ {
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."); throw new InvalidOperationException("Setter.Property must be set.");
} }
var value = Value; 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) value = template.Build();
{
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);
}
} }
else
{
var source = binding.Initiate(control, Property);
if (source != null) var data = new SetterVisitorData
{ {
var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); target = target,
return BindingOperations.Apply(control, Property, cloned, null); 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(); data.result = new PropertySetterInstance<T>(
data.target,
switch (mode) property,
{ (T)data.value);
case BindingMode.OneTime: }
if (sourceInstance.Observable != null) }
{
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
}
else
{
var activated = new ActivatedValue(activator, sourceInstance.Value, description);
return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
}
case BindingMode.OneWay:
{
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
}
default:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
}
}
void IAvaloniaPropertyVisitor<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 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;
}
} }
} }

168
src/Avalonia.Styling/Styling/Style.cs

@ -3,12 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Metadata; using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -16,15 +16,10 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
public class Style : AvaloniaObject, IStyle, ISetResourceParent public class Style : AvaloniaObject, IStyle, ISetResourceParent
{ {
private static Dictionary<IStyleable, CompositeDisposable> _applied = private IResourceNode? _parent;
new Dictionary<IStyleable, CompositeDisposable>(); private IResourceDictionary? _resources;
private IResourceNode _parent; private List<ISetter>? _setters;
private List<IAnimation>? _animations;
private CompositeDisposable _subscriptions;
private IResourceDictionary _resources;
private IList<IAnimation> _animations;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Style"/> class. /// 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. /// Initializes a new instance of the <see cref="Style"/> class.
/// </summary> /// </summary>
/// <param name="selector">The style selector.</param> /// <param name="selector">The style selector.</param>
public Style(Func<Selector, Selector> selector) public Style(Func<Selector?, Selector> selector)
{ {
Selector = selector(null); Selector = selector(null);
} }
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
@ -53,7 +48,7 @@ namespace Avalonia.Styling
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ?? (Resources = new ResourceDictionary());
set set
{ {
Contract.Requires<ArgumentNullException>(value != null); value = value ?? throw new ArgumentNullException(nameof(value));
var hadResources = false; var hadResources = false;
@ -76,117 +71,45 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets or sets the style's selector. /// Gets or sets the style's selector.
/// </summary> /// </summary>
public Selector Selector { get; set; } public Selector? Selector { get; set; }
/// <summary> /// <summary>
/// Gets or sets the style's setters. /// Gets the style's setters.
/// </summary> /// </summary>
[Content] [Content]
public IList<ISetter> Setters { get; set; } = new List<ISetter>(); public IList<ISetter> Setters => _setters ??= new List<ISetter>();
public IList<IAnimation> Animations
{
get
{
return _animations ?? (_animations = new List<IAnimation>());
}
}
private CompositeDisposable Subscriptions /// <summary>
{ /// Gets the style's animations.
get /// </summary>
{ public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
return _subscriptions ?? (_subscriptions = new CompositeDisposable(2));
}
}
/// <inheritdoc/> /// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent; IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0; bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/> /// <inheritdoc/>
public bool Attach(IStyleable control, IStyleHost container) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
if (Selector != null) target = target ?? throw new ArgumentNullException(nameof(target));
{
var match = Selector.Match(control);
if (match.IsMatch)
{
var controlSubscriptions = GetSubscriptions(control);
var animatable = control as Animatable;
var setters = Setters;
var settersCount = setters.Count;
var animations = Animations;
var animationsCount = animations.Count;
var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); var match = Selector is object ? Selector.Match(target) :
target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
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);
}
return match.Result != SelectorMatchResult.NeverThisType; if (match.IsMatch && (_setters is object || _animations is object))
}
else if (control == container)
{ {
var setters = Setters; var instance = new StyleInstance(this, target, _setters, _animations, match.Activator);
var settersCount = setters.Count; target.StyleApplied(instance);
instance.Start();
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;
} }
return false; return match.Result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGetResource(object key, out object result) public bool TryGetResource(object key, out object? result)
{ {
result = null; result = null;
return _resources?.TryGetResource(key, out result) ?? false; return _resources?.TryGetResource(key, out result) ?? false;
@ -222,46 +145,9 @@ namespace Avalonia.Styling
throw new InvalidOperationException("The Style already has a parent."); throw new InvalidOperationException("The Style already has a parent.");
} }
if (parent == null)
{
Detach();
}
_parent = parent; _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) private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
{ {
ResourcesChanged?.Invoke(this, e); ResourcesChanged?.Invoke(this, e);

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
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);
}
}

25
src/Avalonia.Styling/Styling/Styler.cs

@ -3,35 +3,34 @@
using System; using System;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
public class Styler : IStyler 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); var parent = host.StylingParent;
Contract.Requires<ArgumentNullException>(styleHost != null);
var parentContainer = styleHost.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);
} }
} }
} }

76
src/Avalonia.Styling/Styling/Styles.cs

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

5
src/Avalonia.Styling/Styling/TemplateSelector.cs

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

109
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -3,11 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Text; using System.Text;
using Avalonia.Collections; using Avalonia.Styling.Activators;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
@ -17,13 +16,12 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
internal class TypeNameAndClassSelector : Selector 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 readonly Lazy<List<string>> _classes = new Lazy<List<string>>(() => new List<string>());
private Type _targetType; private Type? _targetType;
private string? _selectorString;
private string _selectorString;
public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType)
{ {
var result = new TypeNameAndClassSelector(previous); var result = new TypeNameAndClassSelector(previous);
result._targetType = targetType; result._targetType = targetType;
@ -32,7 +30,7 @@ namespace Avalonia.Styling
return result; return result;
} }
public static TypeNameAndClassSelector Is(Selector previous, Type targetType) public static TypeNameAndClassSelector Is(Selector? previous, Type targetType)
{ {
var result = new TypeNameAndClassSelector(previous); var result = new TypeNameAndClassSelector(previous);
result._targetType = targetType; result._targetType = targetType;
@ -41,7 +39,7 @@ namespace Avalonia.Styling
return result; return result;
} }
public static TypeNameAndClassSelector ForName(Selector previous, string name) public static TypeNameAndClassSelector ForName(Selector? previous, string name)
{ {
var result = new TypeNameAndClassSelector(previous); var result = new TypeNameAndClassSelector(previous);
result.Name = name; result.Name = name;
@ -49,7 +47,7 @@ namespace Avalonia.Styling
return result; return result;
} }
public static TypeNameAndClassSelector ForClass(Selector previous, string className) public static TypeNameAndClassSelector ForClass(Selector? previous, string className)
{ {
var result = new TypeNameAndClassSelector(previous); var result = new TypeNameAndClassSelector(previous);
result.Classes.Add(className); result.Classes.Add(className);
@ -57,7 +55,7 @@ namespace Avalonia.Styling
return result; return result;
} }
protected TypeNameAndClassSelector(Selector previous) protected TypeNameAndClassSelector(Selector? previous)
{ {
_previous = previous; _previous = previous;
} }
@ -68,10 +66,10 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets the name of the control to match. /// Gets the name of the control to match.
/// </summary> /// </summary>
public string Name { get; set; } public string? Name { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public override Type TargetType => _targetType ?? _previous?.TargetType; public override Type? TargetType => _targetType ?? _previous?.TargetType;
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsCombinator => false; public override bool IsCombinator => false;
@ -130,12 +128,12 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
var observable = new ClassObserver(control.Classes, _classes.Value); var observable = new StyleClassActivator(control.Classes, _classes.Value);
return new SelectorMatch(observable); return new SelectorMatch(observable);
} }
if (!AreClassesMatching(control.Classes, Classes)) if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes))
{ {
return SelectorMatch.NeverThisInstance; return SelectorMatch.NeverThisInstance;
} }
@ -144,7 +142,7 @@ namespace Avalonia.Styling
return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance;
} }
protected override Selector MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
private string BuildSelectorString() private string BuildSelectorString()
{ {
@ -190,80 +188,5 @@ namespace Avalonia.Styling
return builder.ToString(); 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);
}
}
} }
} }

19
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -4,6 +4,7 @@
using Avalonia.Styling; using Avalonia.Styling;
using System; using System;
using Avalonia.Controls; using Avalonia.Controls;
using System.Collections.Generic;
namespace Avalonia.Markup.Xaml.Styling namespace Avalonia.Markup.Xaml.Styling
{ {
@ -67,23 +68,7 @@ namespace Avalonia.Markup.Xaml.Styling
IResourceNode IResourceNode.ResourceParent => _parent; IResourceNode IResourceNode.ResourceParent => _parent;
/// <inheritdoc/> /// <inheritdoc/>
public bool Attach(IStyleable control, IStyleHost container) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host);
{
if (Source != null)
{
return Loaded.Attach(control, container);
}
return false;
}
public void Detach()
{
if (Source != null)
{
Loaded.Detach();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);

2
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

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

2
tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>

2
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>

35
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -284,6 +284,41 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty));
} }
[Fact]
public void Disposing_Style_SetValue_Reverts_To_DefaultValue()
{
Class1 target = new Class1();
var d = target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style);
d.Dispose();
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Disposing_Style_SetValue_Reverts_To_Previous_Style_Value()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style);
var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style);
d.Dispose();
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Disposing_Animation_SetValue_Reverts_To_Previous_Local_Value()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "foo", BindingPriority.LocalValue);
var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation);
d.Dispose();
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
}
private class Class1 : AvaloniaObject private class Class1 : AvaloniaObject
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

8
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -3,6 +3,7 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
@ -123,6 +124,11 @@ namespace Avalonia.Base.UnitTests
OverrideMetadata(typeof(T), metadata); OverrideMetadata(typeof(T), metadata);
} }
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{
throw new NotImplementedException();
}
internal override IDisposable RouteBind( internal override IDisposable RouteBind(
IAvaloniaObject o, IAvaloniaObject o,
IObservable<BindingValue<object>> source, IObservable<BindingValue<object>> source,
@ -146,7 +152,7 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException(); throw new NotImplementedException();
} }
internal override void RouteSetValue( internal override IDisposable RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority) BindingPriority priority)

6
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -24,7 +24,11 @@ namespace Avalonia.Base.UnitTests
Owner, Owner,
TestProperty, TestProperty,
NullSink, NullSink,
new ConstantValueEntry<string>(TestProperty, "1", BindingPriority.StyleTrigger)); new ConstantValueEntry<string>(
TestProperty,
"1",
BindingPriority.StyleTrigger,
NullSink));
Assert.Equal("1", target.Value.Value); Assert.Equal("1", target.Value.Value);
Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority);

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>

2
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling
{ {
_window.Styles.Add(new Style(x => x.OfType<TextBox>().Class("foo").Class("bar").Class("baz")) _window.Styles.Add(new Style(x => x.OfType<TextBox>().Class("foo").Class("bar").Class("baz"))
{ {
Setters = new[] Setters =
{ {
new Setter(TextBox.TextProperty, "foo"), new Setter(TextBox.TextProperty, "foo"),
} }

6
tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -34,9 +35,8 @@ namespace Avalonia.Benchmarks.Styling
{ {
var styles = UnitTestApplication.Current.Styles; var styles = UnitTestApplication.Current.Styles;
styles.Attach(_control, UnitTestApplication.Current); styles.TryAttach(_control, UnitTestApplication.Current);
((IStyleable)_control).DetachStyles();
styles.Detach();
} }
public void Dispose() public void Dispose()

2
tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>

2
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>

8
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -363,7 +363,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
new Style(x => x.OfType<TestTemplatedControl>()) new Style(x => x.OfType<TestTemplatedControl>())
{ {
Setters = new[] Setters =
{ {
new Setter( new Setter(
TemplatedControl.TemplateProperty, TemplatedControl.TemplateProperty,
@ -399,7 +399,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
new Style(x => x.OfType<TestTemplatedControl>()) new Style(x => x.OfType<TestTemplatedControl>())
{ {
Setters = new[] Setters =
{ {
new Setter( new Setter(
TemplatedControl.TemplateProperty, TemplatedControl.TemplateProperty,
@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
new Style(x => x.OfType<TestTemplatedControl>()) new Style(x => x.OfType<TestTemplatedControl>())
{ {
Setters = new[] Setters =
{ {
new Setter( new Setter(
TemplatedControl.TemplateProperty, TemplatedControl.TemplateProperty,
@ -458,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
new Style(x => x.OfType<TestTemplatedControl>()) new Style(x => x.OfType<TestTemplatedControl>())
{ {
Setters = new[] Setters =
{ {
new Setter( new Setter(
TemplatedControl.TemplateProperty, TemplatedControl.TemplateProperty,

2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -163,7 +163,7 @@ namespace Avalonia.Controls.UnitTests
{ {
new Style(x => x.OfType<TabItem>()) new Style(x => x.OfType<TabItem>())
{ {
Setters = new[] Setters =
{ {
new Setter(TemplatedControl.TemplateProperty, template) new Setter(TemplatedControl.TemplateProperty, template)
} }

2
tests/Avalonia.Controls.UnitTests/UserControlTests.cs

@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests
{ {
new Style(x => x.OfType<UserControl>()) new Style(x => x.OfType<UserControl>())
{ {
Setters = new[] Setters =
{ {
new Setter(TemplatedControl.TemplateProperty, GetTemplate()) new Setter(TemplatedControl.TemplateProperty, GetTemplate())
} }

2
tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" /> <Import Project="..\..\build\UnitTests.NetCore.targets" />

6
tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs

@ -18,7 +18,7 @@ namespace Avalonia.DesignerSupport.Tests
{ {
public class DesignerSupportTests public class DesignerSupportTests
{ {
private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp2.0/Avalonia.Designer.HostApp.dll"; private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp3.1/Avalonia.Designer.HostApp.dll";
private readonly Xunit.Abstractions.ITestOutputHelper outputHelper; private readonly Xunit.Abstractions.ITestOutputHelper outputHelper;
public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper) public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper)
@ -28,12 +28,12 @@ namespace Avalonia.DesignerSupport.Tests
[SkippableTheory, [SkippableTheory,
InlineData( InlineData(
@"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp2.0/", @"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp3.1/",
"Avalonia.DesignerSupport.TestApp", "Avalonia.DesignerSupport.TestApp",
"Avalonia.DesignerSupport.TestApp.dll", "Avalonia.DesignerSupport.TestApp.dll",
@"..\..\..\..\..\tests\Avalonia.DesignerSupport.TestApp\MainWindow.xaml"), @"..\..\..\..\..\tests\Avalonia.DesignerSupport.TestApp\MainWindow.xaml"),
InlineData( InlineData(
@"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp2.0\", @"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp3.1\",
"ControlCatalog.NetCore", "ControlCatalog.NetCore",
"ControlCatalog.dll", "ControlCatalog.dll",
@"..\..\..\..\..\samples\ControlCatalog\MainWindow.xaml")] @"..\..\..\..\..\samples\ControlCatalog\MainWindow.xaml")]

2
tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" /> <Compile Include="..\Avalonia.RenderTests\**\*.cs" />

2
tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" /> <Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" /> <Import Project="..\..\build\Moq.props" />

2
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>

3
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>

2
tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" /> <Import Project="..\..\build\UnitTests.NetCore.targets" />

2
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>

2
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>

10
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@ -139,7 +139,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
get { throw new NotImplementedException(); } get { throw new NotImplementedException(); }
} }
IObservable<IStyleable> IStyleable.StyleDetach { get; } public void DetachStyles()
{
throw new NotImplementedException();
}
public void StyleApplied(IStyleInstance instance)
{
throw new NotImplementedException();
}
} }
private class AttachedOwner private class AttachedOwner

37
tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs

@ -53,42 +53,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
} }
}; };
setter.Apply(null, control, null); setter.Instance(control).Start(false);
Assert.Equal("foo", control.Text);
control.Text = "bar";
Assert.Equal("bar", data.Foo);
}
}
[Fact]
public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var data = new Data
{
Foo = "foo",
};
var control = new TextBox
{
DataContext = data,
};
var setter = new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
};
var activator = Observable.Never<bool>().StartWith(true);
setter.Apply(null, control, activator);
Assert.Equal("foo", control.Text); Assert.Equal("foo", control.Text);
control.Text = "bar"; control.Text = "bar";

2
tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" /> <Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" /> <Import Project="..\..\build\Moq.props" />

2
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<DefineConstants>AVALONIA_SKIA;AVALONIA_SKIA_SKIP_FAIL</DefineConstants> <DefineConstants>AVALONIA_SKIA;AVALONIA_SKIA_SKIP_FAIL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" /> <Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" /> <Import Project="..\..\build\Moq.props" />

71
tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs

@ -1,71 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedObservableTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
source.OnNext(2);
activator.OnNext(false);
source.OnNext(3);
activator.OnNext(true);
Assert.Equal(
new[]
{
AvaloniaProperty.UnsetValue,
1,
2,
AvaloniaProperty.UnsetValue,
3,
},
result);
}
[Fact]
public void Should_Complete_When_Source_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
source.OnCompleted();
Assert.True(completed);
}
[Fact]
public void Should_Error_When_Source_Errors()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var error = new Exception();
var completed = false;
target.Subscribe(_ => { }, x => completed = true);
source.OnError(error);
Assert.True(completed);
}
}
}

92
tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs

@ -1,92 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedSubjectTests
{
[Fact]
public void Should_Set_Values()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
target.OnNext("bar");
Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
activator.OnNext(true);
target.OnNext("baz");
Assert.Equal("baz", source.Value);
activator.OnNext(false);
Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
target.OnNext("bax");
activator.OnNext(true);
Assert.Equal("bax", source.Value);
}
[Fact]
public void Should_Invoke_OnCompleted_On_Activator_Completed()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
activator.OnCompleted();
Assert.True(source.Completed);
}
[Fact]
public void Should_Invoke_OnError_On_Activator_Error()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
var targetError = default(Exception);
var error = new Exception();
target.Subscribe(_ => { }, e => targetError = e);
activator.OnError(error);
Assert.Same(error, source.Error);
Assert.Same(error, targetError);
}
private class TestSubject : ISubject<object>
{
private IObserver<object> _observer;
public bool Completed { get; set; }
public Exception Error { get; set; }
public object Value { get; set; } = AvaloniaProperty.UnsetValue;
public void OnCompleted()
{
Completed = true;
}
public void OnError(Exception error)
{
Error = error;
}
public void OnNext(object value)
{
Value = value;
}
public IDisposable Subscribe(IObserver<object> observer)
{
_observer = observer;
return Disposable.Empty;
}
}
}
}

75
tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs

@ -1,75 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedValueTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
activator.OnNext(false);
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, 1, AvaloniaProperty.UnsetValue }, result);
}
[Fact]
public void Should_Complete_When_Activator_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
activator.OnCompleted();
Assert.True(completed);
}
[Fact]
public void Should_Error_When_Activator_Errors()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var error = new Exception();
var completed = false;
target.Subscribe(_ => { }, x => completed = true);
activator.OnError(error);
Assert.True(completed);
}
[Fact]
public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed()
{
var scheduler = new TestScheduler();
var activator1 = scheduler.CreateColdObservable<bool>();
var activator2 = scheduler.CreateColdObservable<bool>();
var activator = StyleActivator.And(new[] { activator1, activator2 });
var target = new ActivatedValue(activator, 1, string.Empty);
var subscription = target.Subscribe(_ => { });
Assert.Equal(1, activator1.Subscriptions.Count);
Assert.Equal(Subscription.Infinite, activator1.Subscriptions[0].Unsubscribe);
subscription.Dispose();
Assert.Equal(1, activator1.Subscriptions.Count);
Assert.Equal(0, activator1.Subscriptions[0].Unsubscribe);
}
}
}

2
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<NoWarn>CS0067</NoWarn> <NoWarn>CS0067</NoWarn>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>

16
tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

@ -36,7 +36,7 @@ namespace Avalonia.Styling.UnitTests
{ {
var control = new Control1 var control = new Control1
{ {
Classes = new Classes { "foo" }, Classes = { "foo" },
}; };
var target = default(Selector).Class("foo"); var target = default(Selector).Class("foo");
@ -51,7 +51,7 @@ namespace Avalonia.Styling.UnitTests
{ {
var control = new Control1 var control = new Control1
{ {
Classes = new Classes { "bar" }, Classes = { "bar" },
}; };
var target = default(Selector).Class("foo"); var target = default(Selector).Class("foo");
@ -66,7 +66,7 @@ namespace Avalonia.Styling.UnitTests
{ {
var control = new Control1 var control = new Control1
{ {
Classes = new Classes { "foo" }, Classes = { "foo" },
TemplatedParent = new Mock<ITemplatedControl>().Object, TemplatedParent = new Mock<ITemplatedControl>().Object,
}; };
@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests
var control = new Control1(); var control = new Control1();
var target = default(Selector).Class("foo"); var target = default(Selector).Class("foo");
var activator = target.Match(control).Activator; var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1)); Assert.False(await activator.Take(1));
control.Classes.Add("foo"); control.Classes.Add("foo");
@ -95,11 +95,11 @@ namespace Avalonia.Styling.UnitTests
{ {
var control = new Control1 var control = new Control1
{ {
Classes = new Classes { "foo" }, Classes = { "foo" },
}; };
var target = default(Selector).Class("foo"); var target = default(Selector).Class("foo");
var activator = target.Match(control).Activator; var activator = target.Match(control).Activator.ToObservable();
Assert.True(await activator.Take(1)); Assert.True(await activator.Take(1));
control.Classes.Remove("foo"); control.Classes.Remove("foo");
@ -111,7 +111,7 @@ namespace Avalonia.Styling.UnitTests
{ {
var control = new Control1(); var control = new Control1();
var target = default(Selector).Class("foo").Class("bar"); var target = default(Selector).Class("foo").Class("bar");
var activator = target.Match(control).Activator; var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1)); Assert.False(await activator.Take(1));
control.Classes.Add("foo"); control.Classes.Add("foo");
@ -128,7 +128,7 @@ namespace Avalonia.Styling.UnitTests
// Test for #1698 // Test for #1698
var control = new Control1 var control = new Control1
{ {
Classes = new Classes { "foo" }, Classes = { "foo" },
}; };
var target = default(Selector).Class("foo"); var target = default(Selector).Class("foo");

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

Loading…
Cancel
Save