Browse Source

Merge branch 'master' into extract-layout-manager

pull/1014/head
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
25f44687c0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .gitignore
  2. 5
      .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject
  3. 5
      .ncrunch/BindingDemo.net461.v3.ncrunchproject
  4. 5
      .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject
  5. 5
      .ncrunch/Previewer.v3.ncrunchproject
  6. 5
      .ncrunch/RemoteDemo.v3.ncrunchproject
  7. 5
      .ncrunch/RenderDemo.net461.v3.ncrunchproject
  8. 5
      .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject
  9. 5
      .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject
  10. 5
      .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject
  11. 2
      Avalonia.sln
  12. 233
      build.cake
  13. 5
      build/System.Memory.props
  14. 92
      packages.cake
  15. 12
      parameters.cake
  16. 4
      samples/ControlCatalog/SideBar.xaml
  17. 4
      samples/RenderDemo/SideBar.xaml
  18. 192
      src/Avalonia.Base/AvaloniaObject.cs
  19. 81
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  20. 59
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  21. 45
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  22. 5
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  23. 120
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  24. 88
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  25. 11
      src/Avalonia.Base/Data/Core/IndexerNode.cs
  26. 10
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  27. 10
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  28. 5
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  29. 14
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  30. 19
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  31. 16
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  32. 8
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  33. 68
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  34. 11
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  35. 29
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  36. 17
      src/Avalonia.Base/Data/Core/StreamNode.cs
  37. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  38. 4
      src/Avalonia.Base/PriorityValue.cs
  39. 42
      src/Avalonia.Base/Reactive/AvaloniaObservable.cs
  40. 46
      src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs
  41. 52
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  42. 202
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  43. 76
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  44. 85
      src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs
  45. 172
      src/Avalonia.Base/ValueStore.cs
  46. 45
      src/Avalonia.Controls/AppBuilderBase.cs
  47. 111
      src/Avalonia.Controls/Application.cs
  48. 26
      src/Avalonia.Controls/ExitMode.cs
  49. 1
      src/Avalonia.Controls/ItemsControl.cs
  50. 40
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  51. 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  52. 11
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  53. 1
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  54. 63
      src/Avalonia.Controls/Window.cs
  55. 134
      src/Avalonia.Controls/WindowCollection.cs
  56. 39
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  57. 162
      src/Avalonia.Styling/LogicalTree/ControlLocator.cs
  58. 66
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  59. 71
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  60. 111
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  61. 4
      src/Avalonia.Styling/Styling/StyleActivator.cs
  62. 68
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  63. 2
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  64. 2
      src/Avalonia.Themes.Default/DropDown.xaml
  65. 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  66. 4
      src/Avalonia.Themes.Default/ScrollBar.xaml
  67. 10
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  68. 7
      src/Avalonia.Themes.Default/Slider.xaml
  69. 4
      src/Avalonia.Themes.Default/TabControl.xaml
  70. 2
      src/Avalonia.Themes.Default/TextBox.xaml
  71. 4
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  72. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  73. 670
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  74. 67
      src/Avalonia.Visuals/VisualTree/VisualLocator.cs
  75. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  76. 2
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  77. 85
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  78. 51
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  79. 8
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs
  80. 6
      src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs
  81. 44
      src/Markup/Avalonia.Markup/Data/Binding.cs
  82. 178
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  83. 2
      src/OSX/Avalonia.MonoMac/KeyTransform.cs
  84. 3
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  85. 15
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  86. 5
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  87. 4
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  88. 117
      tests/Avalonia.Controls.UnitTests/ApplicationTests.cs
  89. 20
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  90. 18
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  91. 31
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  92. 13
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  93. 14
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
  94. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  95. 7
      tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs
  96. 10
      tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs
  97. 14
      tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs
  98. 24
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs
  99. 29
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

8
.gitignore

@ -176,5 +176,9 @@ nuget
Avalonia.XBuild.sln
project.lock.json
.idea/*
**/obj-Skia/*
**/obj-Direct2D1/*
##################
## BenchmarkDotNet
##################
BenchmarkDotNet.Artifacts/

5
.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject

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

5
.ncrunch/BindingDemo.net461.v3.ncrunchproject

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

5
.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject

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

5
.ncrunch/Previewer.v3.ncrunchproject

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

5
.ncrunch/RemoteDemo.v3.ncrunchproject

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

5
.ncrunch/RenderDemo.net461.v3.ncrunchproject

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

5
.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject

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

5
.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject

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

5
.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject

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

2
Avalonia.sln

@ -398,6 +398,7 @@ Global
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.ActiveCfg = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.Build.0 = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -407,6 +408,7 @@ Global
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.Build.0 = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.Build.0 = Release|Any CPU
{62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU

233
build.cake

@ -6,6 +6,7 @@
#addin "nuget:?package=NuGet.Core&version=2.14.0"
#tool "nuget:?package=NuGet.CommandLine&version=4.3.0"
#tool "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2017.1.20170613.162720"
///////////////////////////////////////////////////////////////////////////////
// TOOLS
///////////////////////////////////////////////////////////////////////////////
@ -98,7 +99,7 @@ Teardown<AvaloniaBuildData>((context, buildContext) =>
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Clean")
Task("Clean-Impl")
.Does<AvaloniaBuildData>(data =>
{
CleanDirectories(data.Parameters.BuildDirs);
@ -108,9 +109,9 @@ Task("Clean")
CleanDirectory(data.Parameters.BinRoot);
});
Task("Restore-NuGet-Packages")
.IsDependentOn("Clean")
Task("Restore-NuGet-Packages-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsRunningOnWindows)
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPlatformNetCoreOnly)
.Does<AvaloniaBuildData>(data =>
{
var maxRetryCount = 5;
@ -148,11 +149,10 @@ void DotNetCoreBuild(Parameters parameters)
DotNetCoreBuild(parameters.MSBuildSolution, settings);
}
Task("Build")
.IsDependentOn("Restore-NuGet-Packages")
Task("Build-Impl")
.Does<AvaloniaBuildData>(data =>
{
if(data.Parameters.IsRunningOnWindows)
if(data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly)
{
MSBuild(data.Parameters.MSBuildSolution, settings => {
settings.SetConfiguration(data.Parameters.Configuration);
@ -171,7 +171,6 @@ Task("Build")
}
});
void RunCoreTest(string project, Parameters parameters, bool coreOnly = false)
{
if(!project.EndsWith(".csproj"))
@ -194,94 +193,91 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false)
}
}
Task("Run-Unit-Tests")
.IsDependentOn("Build")
Task("Run-Unit-Tests-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests)
.Does<AvaloniaBuildData>(data => {
RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
if (data.Parameters.IsRunningOnWindows)
{
RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);
}
});
.Does<AvaloniaBuildData>(data =>
{
RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
if (data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly)
{
RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);
}
});
Task("Run-Designer-Tests")
.IsDependentOn("Build")
Task("Run-Designer-Tests-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests)
.Does<AvaloniaBuildData>(data => {
RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false);
});
.Does<AvaloniaBuildData>(data =>
{
RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false);
});
Task("Run-Render-Tests")
.IsDependentOn("Build")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows)
.Does<AvaloniaBuildData>(data => {
RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true);
RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true);
});
Task("Run-Render-Tests-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests)
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsRunningOnWindows)
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPlatformNetCoreOnly)
.Does<AvaloniaBuildData>(data =>
{
RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true);
RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true);
});
Task("Run-Leak-Tests")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows)
.IsDependentOn("Build")
Task("Run-Leak-Tests-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.SkipTests)
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsRunningOnWindows)
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPlatformNetCoreOnly)
.Does(() =>
{
var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe");
var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings
{
var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe");
var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings
{
Arguments = new ProcessArgumentBuilder()
.Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath)
.Append("--propagate-exit-code")
.Append("--")
.Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"),
Timeout = 120000
});
if (leakTestsExitCode != 0)
{
throw new Exception("Leak Tests failed");
}
Arguments = new ProcessArgumentBuilder()
.Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath)
.Append("--propagate-exit-code")
.Append("--")
.Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"),
Timeout = 120000
});
Task("Run-Tests")
.IsDependentOn("Run-Unit-Tests")
.IsDependentOn("Run-Render-Tests")
.IsDependentOn("Run-Designer-Tests")
.IsDependentOn("Run-Leak-Tests");
if (leakTestsExitCode != 0)
{
throw new Exception("Leak Tests failed");
}
});
Task("Copy-Files")
.IsDependentOn("Run-Tests")
Task("Copy-Files-Impl")
.Does<AvaloniaBuildData>(data =>
{
CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot);
});
Task("Zip-Files")
.IsDependentOn("Copy-Files")
Task("Zip-Files-Impl")
.Does<AvaloniaBuildData>(data =>
{
Zip(data.Parameters.BinRoot, data.Parameters.ZipCoreArtifacts);
Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs,
data.Parameters.ZipTargetControlCatalogDesktopDirs,
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe"));
Zip(data.Parameters.NugetRoot, data.Parameters.ZipNuGetArtifacts);
if (!data.Parameters.IsPlatformNetCoreOnly) {
Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs,
data.Parameters.ZipTargetControlCatalogDesktopDirs,
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") +
GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe"));
}
});
Task("Create-NuGet-Packages")
.IsDependentOn("Run-Tests")
.IsDependentOn("Inspect")
Task("Create-NuGet-Packages-Impl")
.Does<AvaloniaBuildData>(data =>
{
foreach(var nuspec in data.Packages.NuspecNuGetSettings)
@ -290,8 +286,7 @@ Task("Create-NuGet-Packages")
}
});
Task("Publish-MyGet")
.IsDependentOn("Create-NuGet-Packages")
Task("Publish-MyGet-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsLocalBuild)
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPullRequest)
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsMainRepo)
@ -324,8 +319,7 @@ Task("Publish-MyGet")
Information("Publish-MyGet Task failed, but continuing with next Task...");
});
Task("Publish-NuGet")
.IsDependentOn("Create-NuGet-Packages")
Task("Publish-NuGet-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsLocalBuild)
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPullRequest)
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsMainRepo)
@ -357,54 +351,67 @@ Task("Publish-NuGet")
Information("Publish-NuGet Task failed, but continuing with next Task...");
});
Task("Inspect")
Task("Inspect-Impl")
.WithCriteria<AvaloniaBuildData>((context, data) => data.Parameters.IsRunningOnWindows)
.IsDependentOn("Restore-NuGet-Packages")
.WithCriteria<AvaloniaBuildData>((context, data) => !data.Parameters.IsPlatformNetCoreOnly)
.Does(() =>
{
var badIssues = new []{"PossibleNullReferenceException"};
var whitelist = new []{"tests", "src\\android", "src\\ios",
"src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"};
Information("Running code inspections");
var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"),
new ProcessSettings
{
Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln",
RedirectStandardOutput = true
});
{
var badIssues = new []{"PossibleNullReferenceException"};
var whitelist = new []{"tests", "src\\android", "src\\ios",
"src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"};
Information("Running code inspections");
var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"),
new ProcessSettings
{
Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln",
RedirectStandardOutput = true
});
Information("Analyzing report");
var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml"));
var failBuild = false;
foreach(var xml in doc.Descendants("Issue"))
Information("Analyzing report");
var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml"));
var failBuild = false;
foreach(var xml in doc.Descendants("Issue"))
{
var typeId = xml.Attribute("TypeId").Value.ToString();
if(badIssues.Contains(typeId))
{
var typeId = xml.Attribute("TypeId").Value.ToString();
if(badIssues.Contains(typeId))
{
var file = xml.Attribute("File").Value.ToString().ToLower();
if(whitelist.Any(wh => file.StartsWith(wh)))
continue;
var line = xml.Attribute("Line").Value.ToString();
Error(typeId + " - " + file + " on line " + line);
failBuild = true;
}
var file = xml.Attribute("File").Value.ToString().ToLower();
if(whitelist.Any(wh => file.StartsWith(wh)))
continue;
var line = xml.Attribute("Line").Value.ToString();
Error(typeId + " - " + file + " on line " + line);
failBuild = true;
}
if(failBuild)
throw new Exception("Issues found");
});
}
if(failBuild)
throw new Exception("Issues found");
});
///////////////////////////////////////////////////////////////////////////////
// TARGETS
///////////////////////////////////////////////////////////////////////////////
Task("Run-Tests")
.IsDependentOn("Clean-Impl")
.IsDependentOn("Restore-NuGet-Packages-Impl")
.IsDependentOn("Build-Impl")
.IsDependentOn("Run-Unit-Tests-Impl")
.IsDependentOn("Run-Render-Tests-Impl")
.IsDependentOn("Run-Designer-Tests-Impl")
.IsDependentOn("Run-Leak-Tests-Impl");
Task("Package")
.IsDependentOn("Create-NuGet-Packages");
.IsDependentOn("Run-Tests")
.IsDependentOn("Inspect-Impl")
.IsDependentOn("Create-NuGet-Packages-Impl");
Task("AppVeyor")
.IsDependentOn("Zip-Files")
.IsDependentOn("Publish-MyGet")
.IsDependentOn("Publish-NuGet");
.IsDependentOn("Package")
.IsDependentOn("Copy-Files-Impl")
.IsDependentOn("Zip-Files-Impl")
.IsDependentOn("Publish-MyGet-Impl")
.IsDependentOn("Publish-NuGet-Impl");
Task("Travis")
.IsDependentOn("Run-Tests");

5
build/System.Memory.props

@ -0,0 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.0" />
</ItemGroup>
</Project>

92
packages.cake

@ -1,3 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
public class Packages
@ -9,12 +12,11 @@ public class Packages
public string SkiaSharpVersion {get; private set; }
public string SkiaSharpLinuxVersion {get; private set; }
public Dictionary<string, IList<Tuple<string,string>>> PackageVersions{get; private set;}
class DependencyBuilder : List<NuSpecDependency>
{
Packages _parent;
public DependencyBuilder(Packages parent)
{
_parent = parent;
@ -24,8 +26,7 @@ public class Packages
{
return _parent.PackageVersions[name].First().Item1;
}
public DependencyBuilder Dep(string name, params string[] fws)
{
if(fws.Length == 0)
@ -212,17 +213,33 @@ public class Packages
};
});
var toolsContent = new[] {
new NuSpecContent{
Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath,
Target = "tools/netcoreapp2.0/previewer"
},
new NuSpecContent{
Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath,
Target = "tools/net461/previewer"
}
var toolHostApp = new NuSpecContent{
Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath,
Target = "tools/netcoreapp2.0/previewer"
};
var toolHostAppNetFx = new NuSpecContent{
Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath,
Target = "tools/net461/previewer"
};
IList<NuSpecContent> coreFiles;
if (!parameters.IsPlatformNetCoreOnly) {
var toolsContent = new[] { toolHostApp, toolHostAppNetFx };
coreFiles = coreLibrariesNuSpecContent
.Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform)
.Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform)
.Concat(toolsContent)
.ToList();
} else {
var toolsContent = new[] { toolHostApp };
coreFiles = coreLibrariesNuSpecContent
.Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform)
.Concat(toolsContent)
.ToList();
}
var nuspecNuGetSettingsCore = new []
{
///////////////////////////////////////////////////////////////////////////////
@ -253,13 +270,9 @@ public class Packages
}
.Deps(new string[]{null, "netcoreapp2.0"},
"System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives",
"System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter")
"System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory")
.ToArray(),
Files = coreLibrariesNuSpecContent
.Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform)
.Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform)
.Concat(toolsContent)
.ToList(),
Files = coreFiles,
BasePath = context.Directory("./"),
OutputDirectory = parameters.NugetRoot
},
@ -451,22 +464,6 @@ public class Packages
BasePath = context.Directory("./"),
OutputDirectory = parameters.NugetRoot
},
new NuGetPackSettings()
{
Id = "Avalonia.Win32.Interoperability",
Dependencies = new []
{
new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version },
new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version },
},
Files = new []
{
new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" }
},
BasePath = context.Directory("./src/Windows"),
OutputDirectory = parameters.NugetRoot
},
///////////////////////////////////////////////////////////////////////////////
// Avalonia.LinuxFramebuffer
///////////////////////////////////////////////////////////////////////////////
@ -487,11 +484,32 @@ public class Packages
}
};
var nuspecNuGetSettingInterop = new NuGetPackSettings()
{
Id = "Avalonia.Win32.Interoperability",
Dependencies = new []
{
new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version },
new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version },
new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version },
},
Files = new []
{
new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" }
},
BasePath = context.Directory("./src/Windows"),
OutputDirectory = parameters.NugetRoot
};
NuspecNuGetSettings = new List<NuGetPackSettings>();
NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore);
NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop);
NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile);
if (!parameters.IsPlatformNetCoreOnly) {
NuspecNuGetSettings.Add(nuspecNuGetSettingInterop);
NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile);
}
NuspecNuGetSettings.ForEach((nuspec) => SetNuGetNuspecCommonProperties(nuspec));

12
parameters.cake

@ -8,11 +8,11 @@ public class Parameters
public string AssemblyInfoPath { get; private set; }
public string ReleasePlatform { get; private set; }
public string ReleaseConfiguration { get; private set; }
public string MSBuildSolution { get; private set; }
public string XBuildSolution { get; private set; }
public string MSBuildSolution { get; private set; }
public bool IsPlatformAnyCPU { get; private set; }
public bool IsPlatformX86 { get; private set; }
public bool IsPlatformX64 { get; private set; }
public bool IsPlatformNetCoreOnly { get; private set; }
public bool IsLocalBuild { get; private set; }
public bool IsRunningOnUnix { get; private set; }
public bool IsRunningOnWindows { get; private set; }
@ -34,6 +34,7 @@ public class Parameters
public DirectoryPathCollection BuildDirs { get; private set; }
public string FileZipSuffix { get; private set; }
public FilePath ZipCoreArtifacts { get; private set; }
public FilePath ZipNuGetArtifacts { get; private set; }
public DirectoryPath ZipSourceControlCatalogDesktopDirs { get; private set; }
public FilePath ZipTargetControlCatalogDesktopDirs { get; private set; }
@ -53,12 +54,12 @@ public class Parameters
ReleasePlatform = "Any CPU";
ReleaseConfiguration = "Release";
MSBuildSolution = "./Avalonia.sln";
XBuildSolution = "./Avalonia.XBuild.sln";
// PARAMETERS
IsPlatformAnyCPU = StringComparer.OrdinalIgnoreCase.Equals(Platform, "Any CPU");
IsPlatformX86 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x86");
IsPlatformX64 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x64");
IsPlatformNetCoreOnly = StringComparer.OrdinalIgnoreCase.Equals(Platform, "NetCoreOnly");
IsLocalBuild = buildSystem.IsLocalBuild;
IsRunningOnUnix = context.IsRunningOnUnix();
IsRunningOnWindows = context.IsRunningOnWindows();
@ -71,7 +72,6 @@ public class Parameters
IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleasePlatform, Platform)
&& StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration);
IsMyGetRelease = !IsTagged && IsReleasable;
// VERSION
Version = context.Argument("force-nuget-version", context.ParseAssemblyInfo(AssemblyInfoPath).AssemblyVersion);
@ -103,14 +103,12 @@ public class Parameters
NugetRoot = ArtifactsDir.Combine("nuget");
ZipRoot = ArtifactsDir.Combine("zip");
BinRoot = ArtifactsDir.Combine("bin");
BuildDirs = context.GetDirectories("**/bin") + context.GetDirectories("**/obj");
DirSuffix = Configuration;
DirSuffixIOS = "iPhone" + "/" + Configuration;
FileZipSuffix = Version + ".zip";
ZipCoreArtifacts = ZipRoot.CombineWithFilePath("Avalonia-" + FileZipSuffix);
ZipNuGetArtifacts = ZipRoot.CombineWithFilePath("Avalonia-NuGet-" + FileZipSuffix);
ZipSourceControlCatalogDesktopDirs = (DirectoryPath)context.Directory("./samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461");
ZipTargetControlCatalogDesktopDirs = ZipRoot.CombineWithFilePath("ControlCatalog.Desktop-" + FileZipSuffix);
}

4
samples/ControlCatalog/SideBar.xaml

@ -8,7 +8,7 @@
<TabStrip Name="PART_TabStrip"
MemberSelector="{x:Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
@ -20,7 +20,7 @@
Margin="8 0 0 0"
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>

4
samples/RenderDemo/SideBar.xaml

@ -9,7 +9,7 @@
<TabStrip Name="PART_TabStrip"
MemberSelector="{x:Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
@ -21,7 +21,7 @@
Margin="8 0 0 0"
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>

192
src/Avalonia.Base/AvaloniaObject.cs

@ -10,6 +10,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
@ -28,17 +29,11 @@ namespace Avalonia
/// </summary>
private IAvaloniaObject _inheritanceParent;
/// <summary>
/// The set values/bindings on this object.
/// </summary>
private readonly Dictionary<AvaloniaProperty, PriorityValue> _values =
new Dictionary<AvaloniaProperty, PriorityValue>();
/// <summary>
/// Maintains a list of direct property binding subscriptions so that the binding source
/// doesn't get collected.
/// </summary>
private List<IDisposable> _directBindings;
private List<DirectBindingSubscription> _directBindings;
/// <summary>
/// Event handler for <see cref="INotifyPropertyChanged"/> implementation.
@ -51,6 +46,7 @@ namespace Avalonia
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values;
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
@ -227,9 +223,20 @@ namespace Avalonia
{
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
}
else if (_values != null)
{
var result = _values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
else
{
return GetValueInternal(property);
return GetDefaultValue(property);
}
}
@ -256,7 +263,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false;
return _values?.IsAnimating(property) ?? false;
}
/// <summary>
@ -273,14 +280,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
PriorityValue value;
if (_values.TryGetValue(property, out value))
{
return value.Value != AvaloniaProperty.UnsetValue;
}
return false;
return _values?.IsSet(property) ?? false;
}
/// <summary>
@ -359,36 +359,15 @@ namespace Avalonia
property,
description);
IDisposable subscription = null;
if (_directBindings == null)
{
_directBindings = new List<IDisposable>();
_directBindings = new List<DirectBindingSubscription>();
}
subscription = source
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => SetDirectValue(property, x));
_directBindings.Add(subscription);
return Disposable.Create(() =>
{
subscription.Dispose();
_directBindings.Remove(subscription);
});
return new DirectBindingSubscription(this, property, source);
}
else
{
PriorityValue v;
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
}
Logger.Verbose(
LogArea.Property,
this,
@ -397,7 +376,12 @@ namespace Avalonia
description,
priority);
return v.Add(source, (int)priority);
if (_values == null)
{
_values = new ValueStore(this);
}
return _values.AddBinding(property, source, priority);
}
}
@ -428,20 +412,12 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
VerifyAccess();
PriorityValue value;
if (_values.TryGetValue(property, out value))
{
value.Revalidate();
}
_values?.Revalidate(property);
}
/// <inheritdoc/>
void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue)
void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
var property = sender.Property;
var priority = (BindingPriority)sender.ValuePriority;
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) :
oldValue;
@ -451,7 +427,7 @@ namespace Avalonia
if (!Equals(oldValue, newValue))
{
RaisePropertyChanged(property, oldValue, newValue, priority);
RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
Logger.Verbose(
LogArea.Property,
@ -460,14 +436,14 @@ namespace Avalonia
property,
oldValue,
newValue,
priority);
(BindingPriority)priority);
}
}
/// <inheritdoc/>
void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification)
void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
UpdateDataValidation(sender.Property, notification);
UpdateDataValidation(property, notification);
}
/// <inheritdoc/>
@ -480,10 +456,7 @@ namespace Avalonia
/// Gets all priority values set on the object.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues()
{
return _values;
}
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
/// <summary>
/// Forces revalidation of properties when a property value changes.
@ -672,68 +645,18 @@ namespace Avalonia
}
}
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The <see cref="PriorityValue"/>.</returns>
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType());
Func<object, object> validate2 = null;
if (validate != null)
{
validate2 = v => validate(this, v);
}
PriorityValue result = new PriorityValue(
this,
property,
property.PropertyType,
validate2);
return result;
}
/// <summary>
/// Gets the default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetDefaultValue(AvaloniaProperty property)
internal object GetDefaultValue(AvaloniaProperty property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValueInternal(property);
return aobj.GetValue(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
}
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value
/// without check for registered as this can slow getting the value
/// this method is intended for internal usage in AvaloniaObject only
/// it's called only after check the property is registered
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
private object GetValueInternal(AvaloniaProperty property)
{
object result = AvaloniaProperty.UnsetValue;
PriorityValue value;
if (_values.TryGetValue(property, out value))
{
result = value.Value;
}
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>
@ -814,21 +737,13 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)"));
}
PriorityValue v;
if (!_values.TryGetValue(property, out v))
if (_values == null)
{
if (value == AvaloniaProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
_values = new ValueStore(this);
}
LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority);
_values.AddValue(property, value, (int)priority);
}
/// <summary>
@ -908,5 +823,38 @@ namespace Avalonia
value,
priority);
}
private class DirectBindingSubscription : IObserver<object>, IDisposable
{
readonly AvaloniaObject _owner;
readonly AvaloniaProperty _property;
IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
AvaloniaProperty property,
IObservable<object> source)
{
_owner = owner;
_property = property;
_owner._directBindings.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
_owner._directBindings.Remove(this);
}
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(object value)
{
var castValue = CastOrDefault(value, _property.PropertyType);
_owner.SetDirectValue(_property, castValue);
}
}
}
}

81
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -36,32 +36,15 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes.
/// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<object>(
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(e.NewValue);
}
};
observer.OnNext(o.GetValue(property));
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
return new AvaloniaPropertyObservable<object>(o, property);
}
/// <summary>
@ -74,51 +57,36 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes.
/// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return o.GetObservable((AvaloniaProperty)property).Cast<T>();
return new AvaloniaPropertyObservable<T>(o, property);
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>
/// An observable which when subscribed pushes the old and new values of the property each
/// time it is changed. Note that the observable returned from this method does not fire
/// with the current value of the property immediately.
/// An observable which when subscribed pushes the property changed event args
/// each time a <see cref="IAvaloniaObject.PropertyChanged"/> event is raised
/// for the specified property.
/// </returns>
public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>(
public static IObservable<AvaloniaPropertyChangedEventArgs> GetPropertyChangedObservable(
this IAvaloniaObject o,
AvaloniaProperty<T> property)
AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<Tuple<T, T>>(
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
return new AvaloniaPropertyChangedObservable(o, property);
}
/// <summary>
@ -166,23 +134,6 @@ namespace Avalonia
o.GetObservable(property));
}
/// <summary>
/// Gets a weak observable for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>An observable.</returns>
public static IObservable<object> GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new WeakPropertyChangedObservable(
new WeakReference<IAvaloniaObject>(o),
property,
GetDescription(o, property));
}
/// <summary>
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
/// </summary>

59
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -2,13 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Collections
@ -43,9 +39,8 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null);
return
collection.GetWeakCollectionChangedObservable()
.Subscribe(e => handler.Invoke(collection, e));
return collection.GetWeakCollectionChangedObservable()
.Subscribe(e => handler(collection, e));
}
/// <summary>
@ -63,18 +58,13 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null);
return
collection.GetWeakCollectionChangedObservable()
.Subscribe(handler);
return collection.GetWeakCollectionChangedObservable().Subscribe(handler);
}
private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>,
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs>
{
private WeakReference<INotifyCollectionChanged> _sourceReference;
private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
private int _count;
public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
{
@ -83,43 +73,28 @@ namespace Avalonia.Collections
public void OnEvent(object sender, NotifyCollectionChangedEventArgs e)
{
_changed.OnNext(e);
PublishNext(e);
}
protected override IDisposable SubscribeCore(IObserver<NotifyCollectionChangedEventArgs> observer)
protected override void Initialize()
{
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{
if (_count++ == 0)
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
}
private void DecrementCount()
protected override void Deinitialize()
{
if (--_count == 0)
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
}
}

45
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -7,21 +7,23 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data.Converters;
using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
/// <summary>
/// Binds to an expression on an object using a type value converter to convert the values
/// that are send and received.
/// that are sent and received.
/// </summary>
public class BindingExpression : ISubject<object>, IDescription
public class BindingExpression : LightweightObservableBase<object>, ISubject<object>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
private readonly BindingPriority _priority;
private readonly Subject<object> _errors = new Subject<object>();
InnerListener _innerListener;
WeakReference<object> _value;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -139,7 +141,7 @@ namespace Avalonia.Data.Core
"IValueConverter should not return non-errored BindingNotification.");
}
_errors.OnNext(notification);
PublishNext(notification);
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
@ -170,12 +172,18 @@ namespace Avalonia.Data.Core
}
}
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
protected override void Initialize() => _innerListener = new InnerListener(this);
protected override void Deinitialize() => _innerListener.Dispose();
protected override void Subscribed(IObserver<object> observer, bool first)
{
return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer);
if (!first && _value != null && _value.TryGetTarget(out var val) == true)
{
observer.OnNext(val);
}
}
/// <inheritdoc/>
private object ConvertValue(object value)
{
var notification = value as BindingNotification;
@ -301,5 +309,28 @@ namespace Avalonia.Data.Core
return a;
}
public class InnerListener : IObserver<object>, IDisposable
{
private readonly BindingExpression _owner;
private readonly IDisposable _dispose;
public InnerListener(BindingExpression owner)
{
_owner = owner;
_dispose = owner._inner.Subscribe(this);
}
public void Dispose() => _dispose.Dispose();
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object value)
{
var converted = _owner.ConvertValue(value);
_owner._value = new WeakReference<object>(converted);
_owner.PublishNext(converted);
}
}
}
}

5
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -9,10 +9,5 @@ namespace Avalonia.Data.Core
internal class EmptyExpressionNode : ExpressionNode
{
public override string Description => ".";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
return Observable.Return(reference.Target);
}
}
}

120
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -2,22 +2,18 @@
// 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.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Data.Core
{
internal abstract class ExpressionNode : ISubject<object>
internal abstract class ExpressionNode
{
private static readonly object CacheInvalid = new object();
protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue);
private WeakReference _target = UnsetReference;
private IDisposable _valueSubscription;
private IObserver<object> _observer;
private Action<object> _subscriber;
private bool _listening;
protected WeakReference LastValue { get; private set; }
@ -33,92 +29,66 @@ namespace Avalonia.Data.Core
var oldTarget = _target?.Target;
var newTarget = value.Target;
var running = _valueSubscription != null;
if (!ReferenceEquals(oldTarget, newTarget))
{
_valueSubscription?.Dispose();
_valueSubscription = null;
if (_listening)
{
StopListening();
}
_target = value;
if (running)
if (_subscriber != null)
{
_valueSubscription = StartListening();
StartListening();
}
}
}
}
public IDisposable Subscribe(IObserver<object> observer)
public void Subscribe(Action<object> subscriber)
{
if (_observer != null)
if (_subscriber != null)
{
throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
}
_observer = observer;
var nextSubscription = Next?.Subscribe(this);
_valueSubscription = StartListening();
return Disposable.Create(() =>
{
_valueSubscription?.Dispose();
_valueSubscription = null;
LastValue = null;
nextSubscription?.Dispose();
_observer = null;
});
_subscriber = subscriber;
Next?.Subscribe(NextValueChanged);
StartListening();
}
void IObserver<object>.OnCompleted()
public void Unsubscribe()
{
throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called.");
}
Next?.Unsubscribe();
void IObserver<object>.OnError(Exception error)
{
throw new AvaloniaInternalException("ExpressionNode.OnError should not be called.");
if (_listening)
{
StopListening();
}
LastValue = null;
_subscriber = null;
}
void IObserver<object>.OnNext(object value)
protected virtual void StartListeningCore(WeakReference reference)
{
NextValueChanged(value);
ValueChanged(reference.Target);
}
protected virtual IObservable<object> StartListeningCore(WeakReference reference)
protected virtual void StopListeningCore()
{
return Observable.Return(reference.Target);
}
protected virtual void NextValueChanged(object value)
{
var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
bindingBroken?.AddNode(Description);
_observer.OnNext(value);
}
private IDisposable StartListening()
{
var target = _target.Target;
IObservable<object> source;
if (target == null)
{
source = Observable.Return(TargetNullNotification());
}
else if (target == AvaloniaProperty.UnsetValue)
{
source = Observable.Empty<object>();
}
else
{
source = StartListeningCore(_target);
}
return source.Subscribe(ValueChanged);
_subscriber(value);
}
private void ValueChanged(object value)
protected void ValueChanged(object value)
{
var notification = value as BindingNotification;
@ -131,24 +101,50 @@ namespace Avalonia.Data.Core
}
else
{
_observer.OnNext(value);
_subscriber(value);
}
}
else
{
LastValue = new WeakReference(notification.Value);
if (Next != null)
{
Next.Target = new WeakReference(notification.Value);
}
if (Next == null || notification.Error != null)
{
_observer.OnNext(value);
_subscriber(value);
}
}
}
private void StartListening()
{
var target = _target.Target;
if (target == null)
{
ValueChanged(TargetNullNotification());
_listening = false;
}
else if (target != AvaloniaProperty.UnsetValue)
{
StartListeningCore(_target);
_listening = true;
}
else
{
_listening = false;
}
}
private void StopListening()
{
StopListeningCore();
}
private BindingNotification TargetNullNotification()
{
return new BindingNotification(

88
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -4,18 +4,17 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
/// <summary>
/// Observes and sets the value of an expression on an object.
/// </summary>
public class ExpressionObserver : ObservableBase<object>, IDescription
public class ExpressionObserver : LightweightObservableBase<object>, IDescription
{
/// <summary>
/// An ordered collection of property accessor plugins that can be used to customize
@ -54,9 +53,9 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node;
private readonly Subject<Unit> _finished;
private readonly object _root;
private IObservable<object> _result;
private object _root;
private IDisposable _rootSubscription;
private WeakReference<object> _value;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -107,7 +106,6 @@ namespace Avalonia.Data.Core
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_root = rootObservable;
}
@ -135,8 +133,6 @@ namespace Avalonia.Data.Core
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter());
}
@ -203,27 +199,26 @@ namespace Avalonia.Data.Core
}
}
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<object> observer)
protected override void Initialize()
{
if (_result == null)
{
var source = (IObservable<object>)_node;
_value = null;
_node.Subscribe(ValueChanged);
StartRoot();
}
if (_finished != null)
{
source = source.TakeUntil(_finished);
}
protected override void Deinitialize()
{
_rootSubscription?.Dispose();
_rootSubscription = null;
_node.Unsubscribe();
}
_result = Observable.Using(StartRoot, _ => source)
.Select(ToWeakReference)
.Publish(UninitializedValue)
.RefCount()
.Where(x => x != UninitializedValue)
.Select(Translate);
protected override void Subscribed(IObserver<object> observer, bool first)
{
if (!first && _value != null && _value.TryGetTarget(out var value))
{
observer.OnNext(value);
}
return _result.Subscribe(observer);
}
private static ExpressionNode Parse(string expression, bool enableDataValidation)
@ -238,42 +233,27 @@ namespace Avalonia.Data.Core
}
}
private static object ToWeakReference(object o)
private void StartRoot()
{
return o is BindingNotification ? o : new WeakReference(o);
}
private object Translate(object o)
{
if (o is WeakReference weak)
if (_root is IObservable<object> observable)
{
return weak.Target;
_rootSubscription = observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
x => PublishCompleted(),
() => PublishCompleted());
}
else if (BindingNotification.ExtractError(o) is MarkupBindingChainException broken)
else
{
broken.Commit(Description);
_node.Target = (WeakReference)_root;
}
return o;
}
private IDisposable StartRoot()
private void ValueChanged(object value)
{
switch (_root)
{
case IObservable<object> observable:
return observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
_ => _finished.OnNext(Unit.Default),
() => _finished.OnNext(Unit.Default));
case WeakReference weak:
_node.Target = weak;
break;
default:
throw new AvaloniaInternalException("The ExpressionObserver._root member should only be either an observable or WeakReference.");
}
return Disposable.Empty;
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description);
_value = new WeakReference<object>(value);
PublishNext(value);
}
}
}

11
src/Avalonia.Base/Data/Core/IndexerNode.cs

@ -17,6 +17,8 @@ namespace Avalonia.Data.Core
{
internal class IndexerNode : SettableNode
{
private IDisposable _subscription;
public IndexerNode(IList<string> arguments)
{
Arguments = arguments;
@ -24,7 +26,7 @@ namespace Avalonia.Data.Core
public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override IObservable<object> StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference reference)
{
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
@ -49,7 +51,12 @@ namespace Avalonia.Data.Core
.Select(_ => GetValue(target)));
}
return Observable.Merge(inputs).StartWith(GetValue(target));
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
}
protected override bool SetTargetValueCore(object value, BindingPriority priority)

10
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -145,15 +145,15 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
protected override void Dispose(bool disposing)
protected override void SubscribeCore()
{
_subscription?.Dispose();
_subscription = null;
_subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
}
protected override void SubscribeCore(IObserver<object> observer)
protected override void UnsubscribeCore()
{
_subscription = Instance?.GetWeakObservable(_property).Subscribe(observer);
_subscription?.Dispose();
_subscription = null;
}
}
}

10
src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs

@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="value">The value.</param>
void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => _inner.Dispose();
/// <summary>
/// Begins listening to the inner <see cref="IPropertyAccessor"/>.
/// </summary>
protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this);
protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged);
/// <inheritdoc/>
protected override void UnsubscribeCore() => _inner.Dispose();
/// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins
protected virtual void InnerValueChanged(object value)
{
var notification = value as BindingNotification ?? new BindingNotification(value);
Observer.OnNext(notification);
PublishValue(notification);
}
}
}

5
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -1,7 +1,6 @@
// 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 Avalonia.Data;
using System;
using System.Reflection;
@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins
}
catch (TargetInvocationException ex)
{
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
}
catch (Exception ex)
{
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError));
}
return false;

14
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins
{
@ -10,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins
/// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/>
/// </summary>
public interface IPropertyAccessor : IObservable<object>, IDisposable
public interface IPropertyAccessor : IDisposable
{
/// <summary>
/// Gets the type of the property.
@ -38,5 +37,16 @@ namespace Avalonia.Data.Core.Plugins
/// True if the property was set; false if the property could not be set.
/// </returns>
bool SetValue(object value, BindingPriority priority);
/// <summary>
/// Subscribes to the value of the member.
/// </summary>
/// <param name="listener">A method that receives the values.</param>
void Subscribe(Action<object> listener);
/// <summary>
/// Unsubscribes to the value of the member.
/// </summary>
void Unsubscribe();
}
}

19
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.Data.Core.Plugins
@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
Observer.OnNext(CreateBindingNotification(Value));
PublishValue(CreateBindingNotification(Value));
}
}
protected override void Dispose(bool disposing)
protected override void SubscribeCore()
{
base.Dispose(disposing);
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
}
base.SubscribeCore();
}
protected override void SubscribeCore(IObserver<object> observer)
protected override void UnsubscribeCore()
{
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
WeakSubscriptionManager.Subscribe(
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
}
base.SubscribeCore(observer);
base.UnsubscribeCore();
}
protected override void InnerValueChanged(object value)
{
base.InnerValueChanged(CreateBindingNotification(value));
PublishValue(CreateBindingNotification(value));
}
private BindingNotification CreateBindingNotification(object value)

16
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -103,7 +103,13 @@ namespace Avalonia.Data.Core.Plugins
}
}
protected override void Dispose(bool disposing)
protected override void SubscribeCore()
{
SendCurrentValue();
SubscribeToChanges();
}
protected override void UnsubscribeCore()
{
var inpc = _reference.Target as INotifyPropertyChanged;
@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins
}
}
protected override void SubscribeCore(IObserver<object> observer)
{
SendCurrentValue();
SubscribeToChanges();
}
private void SendCurrentValue()
{
try
{
var value = Value;
Observer.OnNext(value);
PublishValue(value);
}
catch { }
}

8
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -74,14 +74,18 @@ namespace Avalonia.Data.Core.Plugins
public override bool SetValue(object value, BindingPriority priority) => false;
protected override void SubscribeCore(IObserver<object> observer)
protected override void SubscribeCore()
{
try
{
Observer.OnNext(Value);
PublishValue(Value);
}
catch { }
}
protected override void UnsubscribeCore()
{
}
}
}
}

68
src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs

@ -2,67 +2,75 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins
{
/// <summary>
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
/// </summary>
/// <remarks>
/// <see cref="IPropertyAccessor"/> is an observable that will only be subscribed to one time.
/// In addition, the subscription can be disposed by calling <see cref="Dispose()"/> on the
/// property accessor itself - this prevents needing to hold two references for a subscription.
/// </remarks>
public abstract class PropertyAccessorBase : IPropertyAccessor
{
private Action<object> _listener;
/// <inheritdoc/>
public abstract Type PropertyType { get; }
/// <inheritdoc/>
public abstract object Value { get; }
/// <summary>
/// Stops the subscription.
/// </summary>
public void Dispose() => Dispose(true);
/// <inheritdoc/>
public void Dispose()
{
if (_listener != null)
{
Unsubscribe();
}
}
/// <inheritdoc/>
public abstract bool SetValue(object value, BindingPriority priority);
/// <summary>
/// The currently subscribed observer.
/// </summary>
protected IObserver<object> Observer { get; private set; }
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
public void Subscribe(Action<object> listener)
{
Contract.Requires<ArgumentNullException>(observer != null);
Contract.Requires<ArgumentNullException>(listener != null);
if (Observer != null)
if (_listener != null)
{
throw new InvalidOperationException(
"A property accessor can be subscribed to only once.");
"A member accessor can be subscribed to only once.");
}
Observer = observer;
SubscribeCore(observer);
return this;
_listener = listener;
SubscribeCore();
}
public void Unsubscribe()
{
if (_listener == null)
{
throw new InvalidOperationException(
"The member accessor was not subscribed.");
}
UnsubscribeCore();
_listener = null;
}
/// <summary>
/// Publishes a value to the listener.
/// </summary>
/// <param name="value">The value.</param>
protected void PublishValue(object value) => _listener?.Invoke(value);
/// <summary>
/// Stops listening to the property.
/// When overridden in a derived class, begins listening to the member.
/// </summary>
/// <param name="disposing">
/// True if the <see cref="Dispose()"/> method was called, false if the object is being
/// finalized.
/// </param>
protected virtual void Dispose(bool disposing) => Observer = null;
protected abstract void SubscribeCore();
/// <summary>
/// When overridden in a derived class, begins listening to the property.
/// When overridden in a derived class, stops listening to the member.
/// </summary>
protected abstract void SubscribeCore(IObserver<object> observer);
protected abstract void UnsubscribeCore();
}
}

11
src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs

@ -1,6 +1,4 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins
{
@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
public IDisposable Subscribe(IObserver<object> observer)
public void Subscribe(Action<object> listener)
{
listener(_error);
}
public void Unsubscribe()
{
observer.OnNext(_error);
return Disposable.Empty;
}
}
}

29
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -3,9 +3,7 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
@ -39,7 +37,7 @@ namespace Avalonia.Data.Core
return false;
}
protected override IObservable<object> StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference reference)
{
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
var accessor = plugin?.Start(reference, PropertyName);
@ -55,17 +53,20 @@ namespace Avalonia.Data.Core
}
}
// Ensure that _accessor is set for the duration of the subscription.
return Observable.Using(
() =>
{
_accessor = accessor;
return Disposable.Create(() =>
{
_accessor = null;
});
},
_ => accessor);
if (accessor == null)
{
throw new NotSupportedException(
$"Could not find a matching property accessor for {PropertyName}.");
}
accessor.Subscribe(ValueChanged);
_accessor = accessor;
}
protected override void StopListeningCore()
{
_accessor.Dispose();
_accessor = null;
}
}
}

17
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -2,30 +2,37 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Globalization;
using Avalonia.Data;
using System.Reactive.Linq;
namespace Avalonia.Data.Core
{
internal class StreamNode : ExpressionNode
{
private IDisposable _subscription;
public override string Description => "^";
protected override IObservable<object> StartListeningCore(WeakReference reference)
protected override void StartListeningCore(WeakReference reference)
{
foreach (var plugin in ExpressionObserver.StreamHandlers)
{
if (plugin.Match(reference))
{
return plugin.Start(reference);
_subscription = plugin.Start(reference).Subscribe(ValueChanged);
return;
}
}
// TODO: Improve error.
return Observable.Return(new BindingNotification(
ValueChanged(new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", Description),
BindingErrorType.Error));
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

9
src/Avalonia.Base/IPriorityValueOwner.cs

@ -13,18 +13,19 @@ namespace Avalonia
/// <summary>
/// Called when a <see cref="PriorityValue"/>'s value changes.
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="property">The the property that has changed.</param>
/// <param name="priority">The priority of the value.</param>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
void Changed(PriorityValue sender, object oldValue, object newValue);
void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
/// <summary>
/// Called when a <see cref="BindingNotification"/> is received by a
/// <see cref="PriorityValue"/>.
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="property">The the property that has changed.</param>
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(PriorityValue sender, BindingNotification notification);
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Ensures that the current thread is the UI thread.

4
src/Avalonia.Base/PriorityValue.cs

@ -281,12 +281,12 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(this, old, Value));
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
}
if (notification != null)
{
Owner?.BindingNotificationReceived(this, notification);
Owner?.BindingNotificationReceived(Property, notification);
}
}
else

42
src/Avalonia.Base/Reactive/AvaloniaObservable.cs

@ -1,42 +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;
using System.Reactive.Disposables;
namespace Avalonia.Reactive
{
/// <summary>
/// An <see cref="IObservable{T}"/> with an additional description.
/// </summary>
/// <typeparam name="T">The type of the elements in the sequence.</typeparam>
public class AvaloniaObservable<T> : ObservableBase<T>, IDescription
{
private readonly Func<IObserver<T>, IDisposable> _subscribe;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObservable{T}"/> class.
/// </summary>
/// <param name="subscribe">The subscribe function.</param>
/// <param name="description">The description of the observable.</param>
public AvaloniaObservable(Func<IObserver<T>, IDisposable> subscribe, string description)
{
Contract.Requires<ArgumentNullException>(subscribe != null);
_subscribe = subscribe;
Description = description;
}
/// <summary>
/// Gets the description of the observable.
/// </summary>
public string Description { get; }
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
return _subscribe(observer) ?? Disposable.Empty;
}
}
}

46
src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs

@ -0,0 +1,46 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyChangedObservable :
LightweightObservableBase<AvaloniaPropertyChangedEventArgs>,
IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyChangedObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
PublishNext(e);
}
}
}
}

52
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@ -0,0 +1,52 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyObservable<T> : LightweightObservableBase<T>, IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
private T _value;
public AvaloniaPropertyObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
_value = (T)target.GetValue(_property);
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
protected override void Subscribed(IObserver<T> observer, bool first)
{
observer.OnNext(_value);
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_value = (T)e.NewValue;
PublishNext(_value);
}
}
}
}

202
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
/// <summary>
/// Lightweight base class for observable implementations.
/// </summary>
/// <typeparam name="T">The observable type.</typeparam>
/// <remarks>
/// <see cref="ObservableBase{T}"/> is rather heavyweight in terms of allocations and memory
/// usage. This class provides a more lightweight base for some internal observable types
/// in the Avalonia framework.
/// </remarks>
public abstract class LightweightObservableBase<T> : IObservable<T>
{
private Exception _error;
private List<IObserver<T>> _observers = new List<IObserver<T>>();
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
var first = false;
for (; ; )
{
if (Volatile.Read(ref _observers) == null)
{
if (_error != null)
{
observer.OnError(_error);
}
else
{
observer.OnCompleted();
}
return Disposable.Empty;
}
lock (this)
{
if (_observers == null)
{
continue;
}
first = _observers.Count == 0;
_observers.Add(observer);
break;
}
}
if (first)
{
Initialize();
}
Subscribed(observer, first);
return new RemoveObserver(this, observer);
}
void Remove(IObserver<T> observer)
{
if (Volatile.Read(ref _observers) != null)
{
lock (this)
{
var observers = _observers;
if (observers != null)
{
observers.Remove(observer);
if (observers.Count == 0)
{
observers.TrimExcess();
}
else
{
return;
}
} else
{
return;
}
}
Deinitialize();
}
}
sealed class RemoveObserver : IDisposable
{
LightweightObservableBase<T> _parent;
IObserver<T> _observer;
public RemoveObserver(LightweightObservableBase<T> parent, IObserver<T> observer)
{
_parent = parent;
Volatile.Write(ref _observer, observer);
}
public void Dispose()
{
var observer = _observer;
Interlocked.Exchange(ref _parent, null)?.Remove(observer);
_observer = null;
}
}
protected abstract void Initialize();
protected abstract void Deinitialize();
protected void PublishNext(T value)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
}
foreach (var observer in observers)
{
observer.OnNext(value);
}
}
}
protected void PublishCompleted()
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnCompleted();
}
Deinitialize();
}
}
protected void PublishError(Exception error)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
_error = error;
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnError(error);
}
Deinitialize();
}
}
protected virtual void Subscribed(IObserver<T> observer, bool first)
{
}
}
}

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

@ -0,0 +1,76 @@
using System;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
public abstract class SingleSubscriberObservableBase<T> : IObservable<T>, IDisposable
{
private Exception _error;
private IObserver<T> _observer;
private bool _completed;
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
if (_observer != null)
{
throw new InvalidOperationException("The observable can only be subscribed once.");
}
if (_error != null)
{
observer.OnError(_error);
}
else if (_completed)
{
observer.OnCompleted();
}
else
{
_observer = observer;
Subscribed();
}
return this;
}
void IDisposable.Dispose()
{
Unsubscribed();
_observer = null;
}
protected abstract void Unsubscribed();
protected void PublishNext(T value)
{
_observer?.OnNext(value);
}
protected void PublishCompleted()
{
if (_observer != null)
{
_observer.OnCompleted();
_completed = true;
Unsubscribed();
_observer = null;
}
}
protected void PublishError(Exception error)
{
if (_observer != null)
{
_observer.OnError(error);
_error = error;
Unsubscribed();
_observer = null;
}
}
protected abstract void Subscribed();
}
}

85
src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs

@ -1,85 +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;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Utilities;
namespace Avalonia.Reactive
{
internal class WeakPropertyChangedObservable : ObservableBase<object>,
IWeakSubscriber<AvaloniaPropertyChangedEventArgs>, IDescription
{
private WeakReference<IAvaloniaObject> _sourceReference;
private readonly AvaloniaProperty _property;
private readonly Subject<object> _changed = new Subject<object>();
private int _count;
public WeakPropertyChangedObservable(
WeakReference<IAvaloniaObject> source,
AvaloniaProperty property,
string description)
{
_sourceReference = source;
_property = property;
Description = description;
}
public string Description { get; }
public void OnEvent(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_changed.OnNext(e.NewValue);
}
}
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
if (_count++ == 0)
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
observer.OnNext(instance.GetValue(_property));
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
}
}
private void DecrementCount()
{
if (--_count == 0)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
}
}
}
}

172
src/Avalonia.Base/ValueStore.cs

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private readonly AvaloniaObject _owner;
private readonly Dictionary<AvaloniaProperty, object> _values =
new Dictionary<AvaloniaProperty, object>();
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
}
public IDisposable AddBinding(
AvaloniaProperty property,
IObservable<object> source,
BindingPriority priority)
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
if (priorityValue == null)
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
}
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
}
public void AddValue(AvaloniaProperty property, object value, int priority)
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
if (priorityValue == null)
{
if (priority == (int)BindingPriority.LocalValue)
{
_values[property] = Validate(property, value);
Changed(property, priority, v, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
}
}
}
else
{
if (value == AvaloniaProperty.UnsetValue)
{
return;
}
if (priority == (int)BindingPriority.LocalValue)
{
_values.Add(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
}
}
priorityValue.SetValue(value, priority);
}
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification);
}
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException();
public object GetValue(AvaloniaProperty property)
{
var result = AvaloniaProperty.UnsetValue;
if (_values.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
return result;
}
public bool IsAnimating(AvaloniaProperty property)
{
return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false;
}
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
return false;
}
public void Revalidate(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
}
public void VerifyAccess() => _owner.VerifyAccess();
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
Func<object, object> validate2 = null;
if (validate != null)
{
validate2 = v => validate(_owner, v);
}
PriorityValue result = new PriorityValue(
this,
property,
property.PropertyType,
validate2);
return result;
}
private object Validate(AvaloniaProperty property, object value)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
if (validate != null && value != AvaloniaProperty.UnsetValue)
{
return validate(_owner, value);
}
return value;
}
}
}

45
src/Avalonia.Controls/AppBuilderBase.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
private static bool s_setupWasAlreadyCalled;
/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary>
@ -92,7 +92,7 @@ namespace Avalonia.Controls
};
}
protected TAppBuilder Self => (TAppBuilder) this;
protected TAppBuilder Self => (TAppBuilder)this;
/// <summary>
/// Registers a callback to call before Start is called on the <see cref="Application"/>.
@ -125,7 +125,6 @@ namespace Avalonia.Controls
var window = new TMainWindow();
if (dataContextProvider != null)
window.DataContext = dataContextProvider();
window.Show();
Instance.Run(window);
}
@ -143,7 +142,6 @@ namespace Avalonia.Controls
if (dataContextProvider != null)
mainWindow.DataContext = dataContextProvider();
mainWindow.Show();
Instance.Run(mainWindow);
}
@ -209,25 +207,36 @@ namespace Avalonia.Controls
public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules());
/// <summary>
/// Sets the shutdown mode of the application.
/// </summary>
/// <param name="exitMode">The shutdown mode.</param>
/// <returns></returns>
public TAppBuilder SetExitMode(ExitMode exitMode)
{
Instance.ExitMode = exitMode;
return Self;
}
protected virtual bool CheckSetup => true;
private void SetupAvaloniaModules()
{
var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetLoadedAssemblies()
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == ""
|| attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == ""
|| attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports
select (from export in exports
orderby export.ForWindowingSubsystem.Length descending
orderby export.ForRenderingSubsystem.Length descending
select export).First().ModuleType into moduleType
select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
where constructor.GetParameters().Length == 0 && !constructor.IsStatic
select constructor).Single() into constructor
select (Action)(() => constructor.Invoke(new object[0]));
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == ""
|| attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == ""
|| attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports
select (from export in exports
orderby export.ForWindowingSubsystem.Length descending
orderby export.ForRenderingSubsystem.Length descending
select export).First().ModuleType into moduleType
select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
where constructor.GetParameters().Length == 0 && !constructor.IsStatic
select constructor).Single() into constructor
select (Action)(() => constructor.Invoke(new object[0]));
Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
}

111
src/Avalonia.Controls/Application.cs

@ -43,11 +43,15 @@ namespace Avalonia
private Styles _styles;
private IResourceDictionary _resources;
private CancellationTokenSource _mainLoopCancellationTokenSource;
/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
public Application()
{
Windows = new WindowCollection(this);
OnExit += OnExiting;
}
@ -158,6 +162,40 @@ namespace Avalonia
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary>
/// Gets or sets the <see cref="ExitMode"/>. This property indicates whether the application exits explicitly or implicitly.
/// If <see cref="ExitMode"/> is set to OnExplicitExit the application is only closes if Exit is called.
/// The default is OnLastWindowClose
/// </summary>
/// <value>
/// The shutdown mode.
/// </value>
public ExitMode ExitMode { get; set; }
/// <summary>
/// Gets or sets the main window of the application.
/// </summary>
/// <value>
/// The main window.
/// </value>
public Window MainWindow { get; set; }
/// <summary>
/// Gets the open windows of the application.
/// </summary>
/// <value>
/// The windows.
/// </value>
public WindowCollection Windows { get; }
/// <summary>
/// Gets or sets a value indicating whether this instance is existing.
/// </summary>
/// <value>
/// <c>true</c> if this instance is existing; otherwise, <c>false</c>.
/// </value>
internal bool IsExiting { get; set; }
/// <summary>
/// Initializes the application by loading XAML etc.
/// </summary>
@ -171,19 +209,74 @@ namespace Avalonia
/// <param name="closable">The closable to track</param>
public void Run(ICloseable closable)
{
var source = new CancellationTokenSource();
closable.Closed += OnExiting;
closable.Closed += (s, e) => source.Cancel();
Dispatcher.UIThread.MainLoop(source.Token);
if (_mainLoopCancellationTokenSource != null)
{
throw new Exception("Run should only called once");
}
closable.Closed += (s, e) => Exit();
_mainLoopCancellationTokenSource = new CancellationTokenSource();
Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Runs the application's main loop until some condition occurs that is specified by ExitMode.
/// </summary>
/// <param name="mainWindow">The main window</param>
public void Run(Window mainWindow)
{
if (_mainLoopCancellationTokenSource != null)
{
throw new Exception("Run should only called once");
}
_mainLoopCancellationTokenSource = new CancellationTokenSource();
if (MainWindow == null)
{
if (mainWindow == null)
{
throw new ArgumentNullException(nameof(mainWindow));
}
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
MainWindow = mainWindow;
}
Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Runs the application's main loop until the <see cref="CancellationToken"/> is cancelled.
/// Runs the application's main loop until the <see cref="CancellationToken"/> is canceled.
/// </summary>
/// <param name="token">The token to track</param>
public void Run(CancellationToken token)
{
Dispatcher.UIThread.MainLoop(token);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
@ -191,7 +284,13 @@ namespace Avalonia
/// </summary>
public void Exit()
{
IsExiting = true;
Windows.Clear();
OnExit?.Invoke(this, EventArgs.Empty);
_mainLoopCancellationTokenSource?.Cancel();
}
/// <inheritdoc/>

26
src/Avalonia.Controls/ExitMode.cs

@ -0,0 +1,26 @@
// 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.
namespace Avalonia
{
/// <summary>
/// Enum for ExitMode
/// </summary>
public enum ExitMode
{
/// <summary>
/// Indicates an implicit call to Application.Exit when the last window closes.
/// </summary>
OnLastWindowClose,
/// <summary>
/// Indicates an implicit call to Application.Exit when the main window closes.
/// </summary>
OnMainWindowClose,
/// <summary>
/// Indicates that the application only exits on an explicit call to Application.Exit.
/// </summary>
OnExplicitExit
}
}

1
src/Avalonia.Controls/ItemsControl.cs

@ -155,6 +155,7 @@ namespace Avalonia.Controls
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
Presenter = presenter;
ItemContainerGenerator.Clear();
}
/// <summary>

40
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@ -49,11 +49,9 @@ namespace Avalonia.Controls.Mixins
Contract.Requires<ArgumentNullException>(content != null);
Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
EventHandler<RoutedEventArgs> templateApplied = (s, ev) =>
void TemplateApplied(object s, RoutedEventArgs ev)
{
var sender = s as TControl;
if (sender != null)
if (s is TControl sender)
{
var e = (TemplateAppliedEventArgs)ev;
var presenter = (IControl)e.NameScope.Find(presenterName);
@ -64,12 +62,12 @@ namespace Avalonia.Controls.Mixins
var logicalChildren = logicalChildrenSelector(sender);
var subscription = presenter
.GetObservableWithHistory(ContentPresenter.ChildProperty)
.Subscribe(child => UpdateLogicalChild(
.GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(c => UpdateLogicalChild(
sender,
logicalChildren,
child.Item1,
child.Item2));
logicalChildren,
c.OldValue,
c.NewValue));
UpdateLogicalChild(
sender,
@ -80,18 +78,16 @@ namespace Avalonia.Controls.Mixins
subscriptions.Value.Add(sender, subscription);
}
}
};
}
TemplatedControl.TemplateAppliedEvent.AddClassHandler(
typeof(TControl),
templateApplied,
TemplateApplied,
RoutingStrategies.Direct);
content.Changed.Subscribe(e =>
{
var sender = e.Sender as TControl;
if (sender != null)
if (e.Sender is TControl sender)
{
var logicalChildren = logicalChildrenSelector(sender);
UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue);
@ -100,9 +96,7 @@ namespace Avalonia.Controls.Mixins
Control.TemplatedParentProperty.Changed.Subscribe(e =>
{
var sender = e.Sender as TControl;
if (sender != null)
if (e.Sender is TControl sender)
{
var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl;
logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent);
@ -111,13 +105,9 @@ namespace Avalonia.Controls.Mixins
TemplatedControl.TemplateProperty.Changed.Subscribe(e =>
{
var sender = e.Sender as TControl;
if (sender != null)
if (e.Sender is TControl sender)
{
IDisposable subscription;
if (subscriptions.Value.TryGetValue(sender, out subscription))
if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription))
{
subscription.Dispose();
subscriptions.Value.Remove(sender);
@ -134,9 +124,7 @@ namespace Avalonia.Controls.Mixins
{
if (oldValue != newValue)
{
var child = oldValue as IControl;
if (child != null)
if (oldValue is IControl child)
{
logicalChildren.Remove(child);
}

1
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -21,7 +21,6 @@ namespace Avalonia.Controls.Primitives
static AdornerLayer()
{
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
IsHitTestVisibleProperty.OverrideDefaultValue(typeof(AdornerLayer), false);
}
public AdornerLayer()

11
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives
var panel = (InputElement)Presenter.Panel;
foreach (var container in e.Containers)
if (panel != null)
{
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
foreach (var container in e.Containers)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
}
}
}

1
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -247,6 +247,7 @@ namespace Avalonia.Controls.Primitives
foreach (var child in this.GetTemplateChildren())
{
child.SetValue(TemplatedParentProperty, null);
((ISetLogicalParent)child).SetParent(null);
}
VisualChildren.Clear();

63
src/Avalonia.Controls/Window.cs

@ -49,14 +49,6 @@ namespace Avalonia.Controls
/// </summary>
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope
{
private static List<Window> s_windows = new List<Window>();
/// <summary>
/// Retrieves an enumeration of all Windows in the currently running application.
/// </summary>
public static IReadOnlyList<Window> OpenWindows => s_windows;
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
/// </summary>
public static readonly StyledProperty<SizeToContent> SizeToContentProperty =
@ -75,7 +67,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Window, bool>(nameof(ShowInTaskbar), true);
/// <summary>
/// Enables or disables the taskbar icon
/// Represents the current window state (normal, minimized, maximized)
/// </summary>
public static readonly StyledProperty<WindowState> WindowStateProperty =
AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState));
@ -117,7 +109,7 @@ namespace Avalonia.Controls
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
(s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue));
(s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
@ -149,7 +141,7 @@ namespace Avalonia.Controls
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
Screens = new Screens(PlatformImpl?.Screen);
}
/// <inheritdoc/>
event EventHandler<NameScopeEventArgs> INameScope.Registered
{
@ -199,7 +191,7 @@ namespace Avalonia.Controls
get { return GetValue(HasSystemDecorationsProperty); }
set { SetValue(HasSystemDecorationsProperty, value); }
}
/// <summary>
/// Enables or disables the taskbar icon
/// </summary>
@ -259,6 +251,26 @@ namespace Avalonia.Controls
/// </summary>
public event EventHandler<CancelEventArgs> Closing;
private static void AddWindow(Window window)
{
if (Application.Current == null)
{
return;
}
Application.Current.Windows.Add(window);
}
private static void RemoveWindow(Window window)
{
if (Application.Current == null)
{
return;
}
Application.Current.Windows.Remove(window);
}
/// <summary>
/// Closes the window.
/// </summary>
@ -290,19 +302,17 @@ namespace Avalonia.Controls
internal void Close(bool ignoreCancel)
{
var cancelClosing = false;
try
{
cancelClosing = HandleClosing();
if (!ignoreCancel && HandleClosing())
{
return;
}
}
finally
{
if (ignoreCancel || !cancelClosing)
{
s_windows.Remove(this);
PlatformImpl?.Dispose();
IsVisible = false;
}
PlatformImpl?.Dispose();
HandleClosed();
}
}
@ -313,6 +323,7 @@ namespace Avalonia.Controls
{
var args = new CancelEventArgs();
Closing?.Invoke(this, args);
return args.Cancel;
}
@ -359,10 +370,9 @@ namespace Avalonia.Controls
return;
}
s_windows.Add(this);
AddWindow(this);
EnsureInitialized();
SetWindowStartupLocation();
IsVisible = true;
LayoutManager.ExecuteInitialLayoutPass(this);
@ -371,6 +381,7 @@ namespace Avalonia.Controls
PlatformImpl?.Show();
Renderer?.Start();
}
SetWindowStartupLocation();
}
/// <summary>
@ -400,7 +411,7 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The window is already being shown.");
}
s_windows.Add(this);
AddWindow(this);
EnsureInitialized();
SetWindowStartupLocation();
@ -409,7 +420,7 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList();
var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList();
var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault();
SetIsEnabled(affectedWindows, false);
@ -513,8 +524,8 @@ namespace Avalonia.Controls
protected override void HandleClosed()
{
IsVisible = false;
s_windows.Remove(this);
RemoveWindow(this);
base.HandleClosed();
}

134
src/Avalonia.Controls/WindowCollection.cs

@ -0,0 +1,134 @@
// 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.Collections;
using System.Collections.Generic;
using Avalonia.Controls;
namespace Avalonia
{
public class WindowCollection : IReadOnlyList<Window>
{
private readonly Application _application;
private readonly List<Window> _windows = new List<Window>();
public WindowCollection(Application application)
{
_application = application;
}
/// <inheritdoc />
/// <summary>
/// Gets the number of elements in the collection.
/// </summary>
public int Count => _windows.Count;
/// <inheritdoc />
/// <summary>
/// Gets the <see cref="T:Avalonia.Controls.Window" /> at the specified index.
/// </summary>
/// <value>
/// The <see cref="T:Avalonia.Controls.Window" />.
/// </value>
/// <param name="index">The index.</param>
/// <returns></returns>
public Window this[int index] => _windows[index];
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<Window> GetEnumerator()
{
return _windows.GetEnumerator();
}
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Adds the specified window.
/// </summary>
/// <param name="window">The window.</param>
internal void Add(Window window)
{
if (window == null)
{
return;
}
_windows.Add(window);
}
/// <summary>
/// Removes the specified window.
/// </summary>
/// <param name="window">The window.</param>
internal void Remove(Window window)
{
if (window == null)
{
return;
}
_windows.Remove(window);
OnRemoveWindow(window);
}
/// <summary>
/// Closes all windows and removes them from the underlying collection.
/// </summary>
internal void Clear()
{
while (_windows.Count > 0)
{
_windows[0].Close();
}
}
private void OnRemoveWindow(Window window)
{
if (window == null)
{
return;
}
if (_application.IsExiting)
{
return;
}
switch (_application.ExitMode)
{
case ExitMode.OnLastWindowClose:
if (Count == 0)
{
_application.Exit();
}
break;
case ExitMode.OnMainWindowClose:
if (window == _application.MainWindow)
{
_application.Exit();
}
break;
}
}
}
}

39
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@ -1,6 +1,7 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
@ -55,11 +56,39 @@ namespace Avalonia.Controls
public static IObservable<object> GetResourceObservable(this IResourceNode target, string key)
{
return Observable.FromEventPattern<ResourcesChangedEventArgs>(
x => target.ResourcesChanged += x,
x => target.ResourcesChanged -= x)
.StartWith((EventPattern<ResourcesChangedEventArgs>)null)
.Select(x => target.FindResource(key));
return new ResourceObservable(target, key);
}
private class ResourceObservable : LightweightObservableBase<object>
{
private readonly IResourceNode _target;
private readonly string _key;
public ResourceObservable(IResourceNode target, string key)
{
_target = target;
_key = key;
}
protected override void Initialize()
{
_target.ResourcesChanged += ResourcesChanged;
}
protected override void Deinitialize()
{
_target.ResourcesChanged -= ResourcesChanged;
}
protected override void Subscribed(IObserver<object> observer, bool first)
{
observer.OnNext(_target.FindResource(_key));
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
PublishNext(_target.FindResource(_key));
}
}
}
}

162
src/Avalonia.Styling/LogicalTree/ControlLocator.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Reactive;
namespace Avalonia.LogicalTree
{
@ -23,75 +24,122 @@ namespace Avalonia.LogicalTree
/// <param name="name">The name of the control to find.</param>
public static IObservable<ILogical> Track(ILogical relativeTo, string name)
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => ((ILogical)x.Sender).FindNameScope())
.StartWith(relativeTo.FindNameScope());
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => (INameScope)null);
return attached.Merge(detached).Select(nameScope =>
return new ControlTracker(relativeTo, name);
}
public static IObservable<ILogical> Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null)
{
return new ControlTracker(relativeTo, ancestorLevel, ancestorType);
}
private class ControlTracker : LightweightObservableBase<ILogical>
{
private readonly ILogical _relativeTo;
private readonly string _name;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
INameScope _nameScope;
ILogical _value;
public ControlTracker(ILogical relativeTo, string name)
{
_relativeTo = relativeTo;
_name = name;
}
public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType)
{
if (nameScope != null)
_relativeTo = relativeTo;
_ancestorLevel = ancestorLevel;
_ancestorType = ancestorType;
}
protected override void Initialize()
{
Update();
_relativeTo.AttachedToLogicalTree += Attached;
_relativeTo.DetachedFromLogicalTree += Detached;
}
protected override void Deinitialize()
{
_relativeTo.AttachedToLogicalTree -= Attached;
_relativeTo.DetachedFromLogicalTree -= Detached;
if (_nameScope != null)
{
var registered = Observable.FromEventPattern<NameScopeEventArgs>(
x => nameScope.Registered += x,
x => nameScope.Registered -= x)
.Where(x => x.EventArgs.Name == name)
.Select(x => x.EventArgs.Element)
.OfType<ILogical>();
var unregistered = Observable.FromEventPattern<NameScopeEventArgs>(
x => nameScope.Unregistered += x,
x => nameScope.Unregistered -= x)
.Where(x => x.EventArgs.Name == name)
.Select(_ => (ILogical)null);
return registered
.StartWith(nameScope.Find<ILogical>(name))
.Merge(unregistered);
_nameScope.Registered -= Registered;
_nameScope.Unregistered -= Unregistered;
}
else
_value = null;
}
protected override void Subscribed(IObserver<ILogical> observer, bool first)
{
observer.OnNext(_value);
}
private void Attached(object sender, LogicalTreeAttachmentEventArgs e)
{
Update();
PublishNext(_value);
}
private void Detached(object sender, LogicalTreeAttachmentEventArgs e)
{
if (_nameScope != null)
{
return Observable.Return<ILogical>(null);
_nameScope.Registered -= Registered;
_nameScope.Unregistered -= Unregistered;
}
}).Switch();
}
public static IObservable<ILogical> Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null)
{
return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree =>
_value = null;
PublishNext(null);
}
private void Registered(object sender, NameScopeEventArgs e)
{
if (isAttachedToTree)
if (e.Name == _name && e.Element is ILogical logical)
{
return relativeTo.GetLogicalAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel);
_value = logical;
PublishNext(logical);
}
else
}
private void Unregistered(object sender, NameScopeEventArgs e)
{
if (e.Name == _name)
{
return null;
_value = null;
PublishNext(null);
}
});
}
}
private static IObservable<bool> TrackAttachmentToTree(ILogical relativeTo)
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToLogicalTree);
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => false);
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
private void Update()
{
if (_name != null)
{
_nameScope = _relativeTo.FindNameScope();
if (_nameScope != null)
{
_nameScope.Registered += Registered;
_nameScope.Unregistered += Unregistered;
_value = _nameScope.Find<ILogical>(_name);
}
else
{
_value = null;
}
}
else
{
_value = _relativeTo.GetLogicalAncestors()
.Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(_ancestorLevel);
}
}
}
}
}

66
src/Avalonia.Styling/Styling/ActivatedObservable.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Linq;
namespace Avalonia.Styling
{
@ -11,14 +9,16 @@ namespace Avalonia.Styling
/// 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 a
/// 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 : ObservableBase<object>, IDescription
internal class ActivatedObservable : ActivatedValue, IDescription
{
private IDisposable _sourceSubscription;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary>
@ -29,49 +29,49 @@ namespace Avalonia.Styling
IObservable<bool> activator,
IObservable<object> source,
string description)
: base(activator, AvaloniaProperty.UnsetValue, description)
{
Contract.Requires<ArgumentNullException>(activator != null);
Contract.Requires<ArgumentNullException>(source != null);
Activator = activator;
Description = description;
Source = source;
}
/// <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 an observable which produces the <see cref="ActivatedValue"/>.
/// </summary>
public IObservable<object> Source { get; }
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
protected override ActivatorListener CreateListener() => new ValueListener(this);
protected override void Deinitialize()
{
Contract.Requires<ArgumentNullException>(observer != null);
base.Deinitialize();
_sourceSubscription.Dispose();
_sourceSubscription = null;
}
protected override void Initialize()
{
base.Initialize();
_sourceSubscription = Source.Subscribe((ValueListener)Listener);
}
var sourceCompleted = Source.LastOrDefaultAsync().Select(_ => Unit.Default);
var activatorCompleted = Activator.LastOrDefaultAsync().Select(_ => Unit.Default);
var completed = sourceCompleted.Merge(activatorCompleted);
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;
return Activator
.CombineLatest(Source, (x, y) => new { Active = x, Value = y })
.Select(x => x.Active ? x.Value : AvaloniaProperty.UnsetValue)
.DistinctUntilChanged()
.TakeUntil(completed)
.Subscribe(observer);
void IObserver<object>.OnCompleted() => Parent.CompletedReceived();
void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error);
void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value);
}
}
}

71
src/Avalonia.Styling/Styling/ActivatedSubject.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Avalonia.Styling
@ -11,17 +10,14 @@ namespace Avalonia.Styling
/// A subject which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedSubject"/> has two inputs: an activator observable and either an
/// <see cref="ActivatedValue"/> or 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"/>.
/// 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? _active;
private bool _completed;
private object _value;
private object _pushValue;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
@ -35,7 +31,6 @@ namespace Avalonia.Styling
string description)
: base(activator, source, description)
{
Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted);
}
/// <summary>
@ -46,53 +41,57 @@ namespace Avalonia.Styling
get { return (ISubject<object>)base.Source; }
}
/// <summary>
/// Notifies all subscribed observers about the end of the sequence.
/// </summary>
public void OnCompleted()
{
if (_active.Value && !_completed)
{
Source.OnCompleted();
}
Source.OnCompleted();
}
/// <summary>
/// Notifies all subscribed observers with the exception.
/// </summary>
/// <param name="error">The exception to send to all subscribed observers.</param>
/// <exception cref="ArgumentNullException"><paramref name="error"/> is null.</exception>
public void OnError(Exception error)
{
if (_active.Value && !_completed)
{
Source.OnError(error);
}
Source.OnError(error);
}
/// <summary>
/// Notifies all subscribed observers with the value.
/// </summary>
/// <param name="value">The value to send to all subscribed observers.</param>
public void OnNext(object value)
{
_value = value;
_pushValue = value;
if (_active.Value && !_completed)
if (IsActive == true && !_completed)
{
Source.OnNext(value);
Source.OnNext(_pushValue);
}
}
private void ActivatorChanged(bool active)
protected override void ActiveChanged(bool active)
{
bool first = !_active.HasValue;
bool first = !IsActive.HasValue;
_active = active;
base.ActiveChanged(active);
if (!first)
{
Source.OnNext(active ? _value : AvaloniaProperty.UnsetValue);
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;
}
}

111
src/Avalonia.Styling/Styling/ActivatedValue.cs

@ -2,8 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Reactive;
namespace Avalonia.Styling
{
@ -16,12 +15,12 @@ namespace Avalonia.Styling
/// <see cref="ActivatedValue"/> will produce the current value. When the activator
/// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedValue : ObservableBase<object>, IDescription
internal class ActivatedValue : LightweightObservableBase<object>, IDescription
{
/// <summary>
/// The activator.
/// </summary>
private readonly IObservable<bool> _activator;
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.
@ -34,39 +33,101 @@ namespace Avalonia.Styling
object value,
string description)
{
_activator = activator;
Contract.Requires<ArgumentNullException>(activator != null);
Activator = activator;
Value = value;
Description = description;
Listener = CreateListener();
}
/// <summary>
/// Gets the activated value.
/// Gets the activator observable.
/// </summary>
public object Value
{
get;
}
public IObservable<bool> Activator { get; }
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
}
public string Description { get; }
/// <summary>
/// Gets a value indicating whether the activator is active.
/// </summary>
public bool? IsActive { get; private set; }
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// Gets the value that will be produced when <see cref="IsActive"/> is true.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
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)
{
Contract.Requires<ArgumentNullException>(observer != null);
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; }
return _activator
.Select(active => active ? Value : AvaloniaProperty.UnsetValue)
.Subscribe(observer);
void IObserver<bool>.OnCompleted() => Parent.CompletedReceived();
void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error);
void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value);
}
}
}

4
src/Avalonia.Styling/Styling/StyleActivator.cs

@ -48,8 +48,8 @@ namespace Avalonia.Styling
else
{
return inputs.CombineLatest()
.Select(values => values.Any(x => x))
.DistinctUntilChanged();
.Select(values => values.Any(x => x))
.DistinctUntilChanged();
}
}
}

68
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -4,10 +4,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Collections;
using Avalonia.Reactive;
namespace Avalonia.Styling
{
@ -122,14 +122,7 @@ namespace Avalonia.Styling
{
if (subscribe)
{
var observable = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x)
.StartWith((EventPattern<NotifyCollectionChangedEventArgs>)null)
.Select(_ => Matches(control.Classes))
.DistinctUntilChanged();
var observable = new ClassObserver(control.Classes, _classes.Value);
return new SelectorMatch(observable);
}
else
@ -204,5 +197,60 @@ namespace Avalonia.Styling
return builder.ToString();
}
private class ClassObserver : LightweightObservableBase<bool>
{
readonly IList<string> _match;
IAvaloniaReadOnlyList<string> _classes;
bool _value;
public ClassObserver(IAvaloniaReadOnlyList<string> classes, IList<string> match)
{
_classes = classes;
_match = match;
}
protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged;
protected override void Initialize()
{
_value = GetResult();
_classes.CollectionChanged += ClassesChanged;
}
protected override void Subscribed(IObserver<bool> observer, bool first)
{
observer.OnNext(_value);
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Move)
{
var value = GetResult();
if (value != _value)
{
PublishNext(GetResult());
_value = value;
}
}
}
private bool GetResult()
{
int remaining = _match.Count;
foreach (var c in _classes)
{
if (_match.Contains(c))
{
--remaining;
}
}
return remaining == 0;
}
}
}
}

2
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -16,7 +16,7 @@
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" />
<Popup Name="PART_Popup"
MinWidth="{TemplateBinding Bounds.Width}"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">

2
src/Avalonia.Themes.Default/DropDown.xaml

@ -32,7 +32,7 @@
</ToggleButton>
<Popup Name="PART_Popup"
IsOpen="{TemplateBinding IsDropDownOpen, Mode=TwoWay}"
MinWidth="{TemplateBinding Bounds.Width}"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">

4
src/Avalonia.Themes.Default/MenuItem.xaml

@ -45,7 +45,7 @@
<Popup Name="PART_Popup"
PlacementMode="Right"
StaysOpen="True"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
ObeyScreenEdges="True">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
@ -92,7 +92,7 @@
</ContentPresenter.DataTemplates>
</ContentPresenter>
<Popup Name="PART_Popup"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True"
ObeyScreenEdges="True">
<Border Background="{TemplateBinding Background}"

4
src/Avalonia.Themes.Default/ScrollBar.xaml

@ -16,7 +16,7 @@
Grid.Column="1"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Path=Value, Mode=TwoWay}"
Value="{TemplateBinding Value, Mode=TwoWay}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}">
<Track.DecreaseButton>
@ -67,7 +67,7 @@
Grid.Column="1"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Path=Value, Mode=TwoWay}"
Value="{TemplateBinding Value, Mode=TwoWay}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}">
<Track.DecreaseButton>

10
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -9,21 +9,21 @@
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Path=Extent, Mode=TwoWay}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Path=Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Path=Viewport, Mode=TwoWay}"/>
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding Path=HorizontalScrollBarValue, Mode=TwoWay}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"/>
<ScrollBar Name="verticalScrollBar"
Orientation="Vertical"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding Path=VerticalScrollBarValue, Mode=TwoWay}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"/>

7
src/Avalonia.Themes.Default/Slider.xaml

@ -11,7 +11,7 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Name="TrackBackground" Grid.Row="1" Height="4" Margin="6,0" VerticalAlignment="Center"/>
<Track Name="PART_Track" Grid.Row="1">
<Track Name="PART_Track" Grid.Row="1" Orientation="Horizontal">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" />
@ -46,7 +46,7 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/>
<Track Name="PART_Track" Grid.Column="1">
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" />
@ -72,8 +72,7 @@
<Style Selector="Slider /template/ Track#PART_Track">
<Setter Property="Minimum" Value="{TemplateBinding Minimum}"/>
<Setter Property="Maximum" Value="{TemplateBinding Maximum}"/>
<Setter Property="Value" Value="{TemplateBinding Path=Value, Mode=TwoWay}"/>
<Setter Property="Orientation" Value="{TemplateBinding Orientation}"/>
<Setter Property="Value" Value="{TemplateBinding Value, Mode=TwoWay}"/>
</Style>
<Style Selector="Slider /template/ Border#TrackBackground">
<Setter Property="BorderThickness" Value="2"/>

4
src/Avalonia.Themes.Default/TabControl.xaml

@ -9,11 +9,11 @@
<TabStrip Name="PART_TabStrip"
MemberSelector="{x:Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}"/>
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"/>
<Carousel Name="PART_Content"
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>

2
src/Avalonia.Themes.Default/TextBox.xaml

@ -36,7 +36,7 @@
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
IsVisible="{TemplateBinding Path=Text, Converter={x:Static StringConverters.NullOrEmpty}}"/>
IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.NullOrEmpty}}"/>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"

4
src/Avalonia.Themes.Default/TreeViewItem.xaml

@ -7,14 +7,12 @@
<Grid ColumnDefinitions="16, Auto">
<ToggleButton Name="expander"
Focusable="False"
IsChecked="{TemplateBinding Path=IsExpanded, Mode=TwoWay}"/>
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"/>
<ContentPresenter Name="PART_HeaderPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Header}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"
TemplatedControl.IsTemplateFocusTarget="True"
Grid.Column="1"/>

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

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

670
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -5,9 +5,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia.Platform;
namespace Avalonia.Media
@ -17,7 +14,6 @@ namespace Avalonia.Media
/// </summary>
public class PathMarkupParser : IDisposable
{
private static readonly string s_separatorPattern;
private static readonly Dictionary<char, Command> s_commands =
new Dictionary<char, Command>
{
@ -37,14 +33,9 @@ namespace Avalonia.Media
private IGeometryContext _geometryContext;
private Point _currentPoint;
private Point? _previousControlPoint;
private bool? _isOpen;
private bool _isOpen;
private bool _isDisposed;
static PathMarkupParser()
{
s_separatorPattern = CreatesSeparatorPattern();
}
/// <summary>
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
/// </summary>
@ -76,18 +67,6 @@ namespace Avalonia.Media
Close
}
/// <summary>
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="pathData">The path data.</param>
public void Parse(string pathData)
{
var normalizedPathData = NormalizeWhiteSpaces(pathData);
var tokens = ParseTokens(normalizedPathData);
CreateGeometry(tokens);
}
void IDisposable.Dispose()
{
Dispose(true);
@ -108,66 +87,6 @@ namespace Avalonia.Media
_isDisposed = true;
}
private static string NormalizeWhiteSpaces(string s)
{
int length = s.Length,
index = 0,
i = 0;
var source = s.ToCharArray();
var skip = false;
for (; i < length; i++)
{
var c = source[i];
if (char.IsWhiteSpace(c))
{
if (skip)
{
continue;
}
source[index++] = c;
skip = true;
continue;
}
skip = false;
source[index++] = c;
}
if (char.IsWhiteSpace(source[index - 1]))
{
index--;
}
return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index);
}
private static string CreatesSeparatorPattern()
{
var stringBuilder = new StringBuilder();
foreach (var command in s_commands.Keys)
{
stringBuilder.Append(command);
stringBuilder.Append(char.ToLower(command));
}
return @"(?=[" + stringBuilder + "])";
}
private static IEnumerable<CommandToken> ParseTokens(string s)
{
var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t));
return expressions.Select(CommandToken.Parse);
}
private static Point MirrorControlPoint(Point controlPoint, Point center)
{
var dir = controlPoint - center;
@ -175,76 +94,78 @@ namespace Avalonia.Media
return center + -dir;
}
private void CreateGeometry(IEnumerable<CommandToken> commandTokens)
/// <summary>
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="pathData">The path data.</param>
public void Parse(string pathData)
{
var span = pathData.AsSpan();
_currentPoint = new Point();
foreach (var commandToken in commandTokens)
while(!span.IsEmpty)
{
try
{
while (true)
{
switch (commandToken.Command)
{
case Command.None:
break;
case Command.FillRule:
SetFillRule(commandToken);
break;
case Command.Move:
AddMove(commandToken);
break;
case Command.Line:
AddLine(commandToken);
break;
case Command.HorizontalLine:
AddHorizontalLine(commandToken);
break;
case Command.VerticalLine:
AddVerticalLine(commandToken);
break;
case Command.CubicBezierCurve:
AddCubicBezierCurve(commandToken);
break;
case Command.QuadraticBezierCurve:
AddQuadraticBezierCurve(commandToken);
break;
case Command.SmoothCubicBezierCurve:
AddSmoothCubicBezierCurve(commandToken);
break;
case Command.SmoothQuadraticBezierCurve:
AddSmoothQuadraticBezierCurve(commandToken);
break;
case Command.Arc:
AddArc(commandToken);
break;
case Command.Close:
CloseFigure();
break;
default:
throw new NotSupportedException("Unsupported command");
}
if (commandToken.HasImplicitCommands)
{
continue;
}
break;
}
}
catch (InvalidDataException)
if(!ReadCommand(ref span, out var command, out var relative))
{
break;
return;
}
catch (NotSupportedException)
bool initialCommand = true;
do
{
break;
}
if (!initialCommand)
{
span = ReadSeparator(span);
}
switch (command)
{
case Command.None:
break;
case Command.FillRule:
SetFillRule(ref span);
break;
case Command.Move:
AddMove(ref span, relative);
break;
case Command.Line:
AddLine(ref span, relative);
break;
case Command.HorizontalLine:
AddHorizontalLine(ref span, relative);
break;
case Command.VerticalLine:
AddVerticalLine(ref span, relative);
break;
case Command.CubicBezierCurve:
AddCubicBezierCurve(ref span, relative);
break;
case Command.QuadraticBezierCurve:
AddQuadraticBezierCurve(ref span, relative);
break;
case Command.SmoothCubicBezierCurve:
AddSmoothCubicBezierCurve(ref span, relative);
break;
case Command.SmoothQuadraticBezierCurve:
AddSmoothQuadraticBezierCurve(ref span, relative);
break;
case Command.Arc:
AddArc(ref span, relative);
break;
case Command.Close:
CloseFigure();
break;
default:
throw new NotSupportedException("Unsupported command");
}
initialCommand = false;
} while (PeekArgument(span));
}
if (_isOpen != null)
if (_isOpen)
{
_geometryContext.EndFigure(false);
}
@ -252,7 +173,7 @@ namespace Avalonia.Media
private void CreateFigure()
{
if (_isOpen != null)
if (_isOpen)
{
_geometryContext.EndFigure(false);
}
@ -262,62 +183,72 @@ namespace Avalonia.Media
_isOpen = true;
}
private void SetFillRule(CommandToken commandToken)
private void SetFillRule(ref ReadOnlySpan<char> span)
{
var fillRule = commandToken.ReadFillRule();
if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1)
{
throw new InvalidDataException("Invalid fill rule.");
}
FillRule rule;
_geometryContext.SetFillRule(fillRule);
switch (fillRule[0])
{
case '0':
rule = FillRule.EvenOdd;
break;
case '1':
rule = FillRule.NonZero;
break;
default:
throw new InvalidDataException("Invalid fill rule");
}
_geometryContext.SetFillRule(rule);
}
private void CloseFigure()
{
if (_isOpen == true)
if (_isOpen)
{
_geometryContext.EndFigure(true);
}
_previousControlPoint = null;
_isOpen = null;
_isOpen = false;
}
private void AddMove(CommandToken commandToken)
private void AddMove(ref ReadOnlySpan<char> span, bool relative)
{
var currentPoint = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var currentPoint = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_currentPoint = currentPoint;
CreateFigure();
if (!commandToken.HasImplicitCommands)
while (PeekArgument(span))
{
return;
}
span = ReadSeparator(span);
AddLine(ref span, relative);
while (commandToken.HasImplicitCommands)
{
AddLine(commandToken);
if (commandToken.IsRelative)
if (!relative)
{
continue;
_currentPoint = currentPoint;
CreateFigure();
}
_currentPoint = currentPoint;
CreateFigure();
}
}
private void AddLine(CommandToken commandToken)
private void AddLine(ref ReadOnlySpan<char> span, bool relative)
{
_currentPoint = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
_currentPoint = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -325,13 +256,13 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint);
}
private void AddHorizontalLine(CommandToken commandToken)
private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
{
_currentPoint = commandToken.IsRelative
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
: _currentPoint.WithX(commandToken.ReadDouble());
_currentPoint = relative
? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
: _currentPoint.WithX(ReadDouble(ref span));
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -339,13 +270,13 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint);
}
private void AddVerticalLine(CommandToken commandToken)
private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
{
_currentPoint = commandToken.IsRelative
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
: _currentPoint.WithY(commandToken.ReadDouble());
_currentPoint = relative
? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
: _currentPoint.WithY(ReadDouble(ref span));
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -353,23 +284,27 @@ namespace Avalonia.Media
_geometryContext.LineTo(_currentPoint);
}
private void AddCubicBezierCurve(CommandToken commandToken)
private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
var point1 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var point1 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
span = ReadSeparator(span);
var point2 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var point2 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_previousControlPoint = point2;
var point3 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
span = ReadSeparator(span);
if (_isOpen == null)
var point3 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
@ -379,19 +314,21 @@ namespace Avalonia.Media
_currentPoint = point3;
}
private void AddQuadraticBezierCurve(CommandToken commandToken)
private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
var start = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var start = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
_previousControlPoint = start;
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
span = ReadSeparator(span);
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -401,22 +338,24 @@ namespace Avalonia.Media
_currentPoint = end;
}
private void AddSmoothCubicBezierCurve(CommandToken commandToken)
private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
var point2 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var point2 = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
span = ReadSeparator(span);
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -428,18 +367,18 @@ namespace Avalonia.Media
_currentPoint = end;
}
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
{
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (_isOpen == null)
if (!_isOpen)
{
CreateFigure();
}
@ -449,21 +388,27 @@ namespace Avalonia.Media
_currentPoint = end;
}
private void AddArc(CommandToken commandToken)
private void AddArc(ref ReadOnlySpan<char> span, bool relative)
{
var size = commandToken.ReadSize();
var size = ReadSize(ref span);
var rotationAngle = commandToken.ReadDouble();
span = ReadSeparator(span);
var isLargeArc = commandToken.ReadBool();
var rotationAngle = ReadDouble(ref span);
span = ReadSeparator(span);
var isLargeArc = ReadBool(ref span);
var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
span = ReadSeparator(span);
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
span = ReadSeparator(span);
if (_isOpen == null)
var end = relative
? ReadRelativePoint(ref span, _currentPoint)
: ReadPoint(ref span);
if (!_isOpen)
{
CreateFigure();
}
@ -475,210 +420,149 @@ namespace Avalonia.Media
_previousControlPoint = null;
}
private class CommandToken
private static bool PeekArgument(ReadOnlySpan<char> span)
{
private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
{
Command = command;
span = SkipWhitespace(span);
IsRelative = isRelative;
Arguments = new List<string>(arguments);
}
public Command Command { get; }
public bool IsRelative { get; }
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
}
public bool HasImplicitCommands
private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)
{
get
{
if (CurrentPosition == 0 && Arguments.Count > 0)
{
return true;
}
return CurrentPosition < Arguments.Count - 1;
}
}
private int CurrentPosition { get; set; }
private List<string> Arguments { get; }
public static CommandToken Parse(string s)
{
using (var reader = new StringReader(s))
{
var command = Command.None;
var isRelative = false;
if (!ReadCommand(reader, ref command, ref isRelative))
{
throw new InvalidDataException("No path command declared.");
}
var commandArguments = reader.ReadToEnd();
var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
var arguments = new List<string>();
foreach (Match match in argumentMatches)
{
arguments.Add(match.Value);
}
return new CommandToken(command, isRelative, arguments);
}
argument = ReadOnlySpan<char>.Empty;
return false;
}
public FillRule ReadFillRule()
var valid = false;
int i = 0;
if (remaining[i] == '-')
{
if (CurrentPosition == Arguments.Count)
{
throw new InvalidDataException("Invalid fill rule");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
case "0":
{
return FillRule.EvenOdd;
}
case "1":
{
return FillRule.NonZero;
}
default:
throw new InvalidDataException("Invalid fill rule");
}
i++;
}
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
public bool ReadBool()
if (i < remaining.Length && remaining[i] == '.')
{
if (CurrentPosition == Arguments.Count)
{
throw new InvalidDataException("Invalid boolean value");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
case "1":
{
return true;
}
case "0":
{
return false;
}
default:
throw new InvalidDataException("Invalid boolean value");
}
valid = false;
i++;
}
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
public double ReadDouble()
if (i < remaining.Length)
{
if (CurrentPosition == Arguments.Count)
// scientific notation
if (remaining[i] == 'E' || remaining[i] == 'e')
{
throw new InvalidDataException("Invalid double value");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
return double.Parse(value, CultureInfo.InvariantCulture);
}
valid = false;
i++;
if (remaining[i] == '-' || remaining[i] == '+')
{
i++;
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
}
}
}
public Size ReadSize()
if (!valid)
{
var width = ReadDouble();
var height = ReadDouble();
return new Size(width, height);
argument = ReadOnlySpan<char>.Empty;
return false;
}
argument = remaining.Slice(0, i);
remaining = remaining.Slice(i);
return true;
}
public Point ReadPoint()
{
var x = ReadDouble();
var y = ReadDouble();
return new Point(x, y);
}
public Point ReadRelativePoint(Point origin)
private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span)
{
span = SkipWhitespace(span);
if (!span.IsEmpty && span[0] == ',')
{
var x = ReadDouble();
var y = ReadDouble();
span = span.Slice(1);
}
return span;
}
return new Point(origin.X + x, origin.Y + y);
}
private static ReadOnlySpan<char> SkipWhitespace(ReadOnlySpan<char> span)
{
int i = 0;
for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ;
return span.Slice(i);
}
private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative)
private bool ReadBool(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1)
{
ReadWhitespace(reader);
var i = reader.Peek();
if (i == -1)
{
throw new InvalidDataException("Invalid bool rule.");
}
switch (boolValue[0])
{
case '0':
return false;
}
case '1':
return true;
default:
throw new InvalidDataException("Invalid bool rule");
}
}
var c = (char)i;
private double ReadDouble(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var doubleValue))
{
throw new InvalidDataException("Invalid double value");
}
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
{
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
}
command = next;
private Size ReadSize(ref ReadOnlySpan<char> span)
{
var width = ReadDouble(ref span);
span = ReadSeparator(span);
var height = ReadDouble(ref span);
return new Size(width, height);
}
relative = char.IsLower(c);
private Point ReadPoint(ref ReadOnlySpan<char> span)
{
var x = ReadDouble(ref span);
span = ReadSeparator(span);
var y = ReadDouble(ref span);
return new Point(x, y);
}
reader.Read();
private Point ReadRelativePoint(ref ReadOnlySpan<char> span, Point origin)
{
var x = ReadDouble(ref span);
span = ReadSeparator(span);
var y = ReadDouble(ref span);
return new Point(origin.X + x, origin.Y + y);
}
return true;
private bool ReadCommand(ref ReadOnlySpan<char> span, out Command command, out bool relative)
{
span = SkipWhitespace(span);
if (span.IsEmpty)
{
command = default;
relative = false;
return false;
}
private static void ReadWhitespace(TextReader reader)
var c = span[0];
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
{
int i;
while ((i = reader.Peek()) != -1)
{
var c = (char)i;
if (char.IsWhiteSpace(c))
{
reader.Read();
}
else
{
break;
}
}
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
relative = char.IsLower(c);
span = span.Slice(1);
return true;
}
}
}

67
src/Avalonia.Visuals/VisualTree/VisualLocator.cs

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.VisualTree
{
@ -11,36 +10,54 @@ namespace Avalonia.VisualTree
{
public static IObservable<IVisual> Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null)
{
return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree =>
return new VisualTracker(relativeTo, ancestorLevel, ancestorType);
}
private class VisualTracker : LightweightObservableBase<IVisual>
{
private readonly IVisual _relativeTo;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
public VisualTracker(IVisual relativeTo, int ancestorLevel, Type ancestorType)
{
_relativeTo = relativeTo;
_ancestorLevel = ancestorLevel;
_ancestorType = ancestorType;
}
protected override void Initialize()
{
_relativeTo.AttachedToVisualTree += AttachedDetached;
_relativeTo.DetachedFromVisualTree += AttachedDetached;
}
protected override void Deinitialize()
{
if (isAttachedToTree)
_relativeTo.AttachedToVisualTree -= AttachedDetached;
_relativeTo.DetachedFromVisualTree -= AttachedDetached;
}
protected override void Subscribed(IObserver<IVisual> observer, bool first)
{
observer.OnNext(GetResult());
}
private void AttachedDetached(object sender, VisualTreeAttachmentEventArgs e) => PublishNext(GetResult());
private IVisual GetResult()
{
if (_relativeTo.IsAttachedToVisualTree)
{
return relativeTo.GetVisualAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel);
return _relativeTo.GetVisualAncestors()
.Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(_ancestorLevel);
}
else
{
return null;
}
});
}
private static IObservable<bool> TrackAttachmentToTree(IVisual relativeTo)
{
var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.AttachedToVisualTree += x,
x => relativeTo.AttachedToVisualTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToVisualTree);
var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromVisualTree += x,
x => relativeTo.DetachedFromVisualTree -= x)
.Select(x => false);
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
}
}
}
}

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -48,7 +48,6 @@
<Compile Include="Converters\SelectorTypeConverter.cs" />
<Compile Include="MarkupExtensions\BindingExtension.cs" />
<Compile Include="MarkupExtensions\RelativeSourceExtension.cs" />
<Compile Include="MarkupExtensions\TemplateBindingExtension.cs" />
<Compile Include="PortableXaml\AvaloniaTypeAttributeProvider.cs" />
<Compile Include="PortableXaml\AvaloniaXamlType.cs" />
<Compile Include="PortableXaml\TypeDescriptorExtensions.cs" />

2
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@ -139,7 +139,7 @@ namespace Avalonia.Markup.Xaml
{
uriString = new Uri(baseUri, uri).AbsoluteUri;
}
throw new XamlLoadException("Error loading xaml at " + uriString, e);
throw new XamlLoadException("Error loading xaml at " + uriString + ": " + e.Message, e);
}
}
}

85
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@ -2,19 +2,21 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Portable.Xaml;
using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters
{
using Avalonia.Styling;
using Portable.Xaml;
using Portable.Xaml.ComponentModel;
using System.ComponentModel;
using Portable.Xaml.Markup;
public class AvaloniaPropertyTypeConverter : TypeConverter
{
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
@ -22,65 +24,58 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var s = (string)value;
var (owner, propertyName) = ParseProperty((string)value);
var ownerType = TryResolveOwnerByName(context, owner) ??
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType;
string typeName;
string propertyName;
Type type = null;
if (ownerType == null)
{
throw new XamlLoadException(
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
}
ParseProperty(s, out typeName, out propertyName);
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName);
if (typeName == null)
if (property == null)
{
var style = context.GetFirstAmbientValue<Style>();
throw new XamlLoadException($"Could not find AvaloniaProperty '{ownerType.Name}.{propertyName}'.");
}
type = style?.Selector?.TargetType;
return property;
}
if (type == null)
{
throw new Exception(
"Could not determine the target type. Please fully qualify the property name.");
}
}
else
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner)
{
if (owner != null)
{
var typeResolver = context.GetService<IXamlTypeResolver>();
type = typeResolver.Resolve(typeName);
var resolver = context.GetService<IXamlTypeResolver>();
var result = resolver.Resolve(owner);
if (type == null)
if (result == null)
{
throw new Exception($"Could not find type '{typeName}'.");
throw new XamlLoadException($"Could not find type '{owner}'.");
}
}
AvaloniaProperty property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, propertyName);
if (property == null)
{
throw new Exception(
$"Could not find AvaloniaProperty '{type.Name}.{propertyName}'.");
return result;
}
return property;
return null;
}
private void ParseProperty(string s, out string typeName, out string propertyName)
private (string owner, string property) ParseProperty(string s)
{
var split = s.Split('.');
var result = regex.Match(s);
if (split.Length == 1)
{
typeName = null;
propertyName = split[0];
}
else if (split.Length == 2)
if (result.Groups[1].Success)
{
typeName = split[0];
propertyName = split[1];
return (result.Groups[1].Value, result.Groups[2].Value);
}
else
{
throw new Exception($"Invalid property name: '{s}'.");
return (null, result.Groups[3].Value);
}
}
}

51
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs

@ -1,51 +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 Avalonia.Data;
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
using System;
using Avalonia.Data.Converters;
using Avalonia.Markup.Data;
using Portable.Xaml.Markup;
[MarkupExtensionReturnType(typeof(IBinding))]
public class TemplateBindingExtension : MarkupExtension
{
public TemplateBindingExtension()
{
}
public TemplateBindingExtension(string path)
{
Path = path;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new Binding
{
Converter = Converter,
ElementName = ElementName,
Mode = Mode,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Path = Path ?? string.Empty,
Priority = Priority,
};
}
public IValueConverter Converter { get; set; }
public string ElementName { get; set; }
public object FallbackValue { get; set; }
public BindingMode Mode { get; set; }
[ConstructorArgument("path")]
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.TemplatedParent;
}
}

8
src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs

@ -44,15 +44,17 @@ namespace Portable.Xaml.ComponentModel
var amb = ctx.GetService<IAmbientProvider>();
var sc = ctx.GetService<IXamlSchemaContextProvider>().SchemaContext;
return amb.GetFirstAmbientValue(sc.GetXamlType(typeof(T))) as T;
// Because GetFirstAmbientValue uses XamlType.CanAssignTo it returns values that
// aren't actually of the correct type. Use GetAllAmbientValues instead.
return amb.GetAllAmbientValues(sc.GetXamlType(typeof(T))).OfType<T>().FirstOrDefault();
}
public static T GetLastOrDefaultAmbientValue<T>(this ITypeDescriptorContext ctx) where T : class
{
return ctx.GetAllambientValues<T>().LastOrDefault() as T;
return ctx.GetAllAmbientValues<T>().LastOrDefault() as T;
}
public static IEnumerable<T> GetAllambientValues<T>(this ITypeDescriptorContext ctx) where T : class
public static IEnumerable<T> GetAllAmbientValues<T>(this ITypeDescriptorContext ctx) where T : class
{
var amb = ctx.GetService<IAmbientProvider>();
var sc = ctx.GetService<IXamlSchemaContextProvider>().SchemaContext;

6
src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs

@ -1,6 +1,7 @@
// 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.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
@ -14,7 +15,8 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent]
public object Content { get; set; }
public IControl Build(ITemplatedControl control)
=> TemplateContent.Load(Content);
public Type TargetType { get; set; }
public IControl Build(ITemplatedControl control) => TemplateContent.Load(Content);
}
}

44
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -5,11 +5,10 @@ using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Data
@ -190,13 +189,10 @@ namespace Avalonia.Data
if (!targetIsDataContext)
{
var update = target.GetObservable(StyledElement.DataContextProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty),
path,
update,
new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation);
return result;
@ -278,14 +274,10 @@ namespace Avalonia.Data
{
Contract.Requires<ArgumentNullException>(target != null);
var update = target.GetObservable(StyledElement.TemplatedParentProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
path,
update,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation);
return result;
@ -306,5 +298,35 @@ namespace Avalonia.Data
Observable.Return((object)null);
}).Switch();
}
private class UpdateSignal : SingleSubscriberObservableBase<Unit>
{
private readonly IAvaloniaObject _target;
private readonly AvaloniaProperty _property;
public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property)
{
_target = target;
_property = property;
}
protected override void Subscribed()
{
_target.PropertyChanged += PropertyChanged;
}
protected override void Unsubscribed()
{
_target.PropertyChanged -= PropertyChanged;
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
PublishNext(Unit.Default);
}
}
}
}
}

178
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -0,0 +1,178 @@
using System;
using System.Globalization;
using System.Reactive.Subjects;
using Avalonia.Data.Converters;
using Avalonia.Reactive;
namespace Avalonia.Data
{
/// <summary>
/// A XAML binding to a property on a control's templated parent.
/// </summary>
public class TemplateBinding : SingleSubscriberObservableBase<object>,
IBinding,
IDescription,
ISubject<object>
{
private IStyledElement _target;
private Type _targetType;
public TemplateBinding()
{
}
public TemplateBinding(AvaloniaProperty property)
{
Property = property;
}
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
bool enableDataValidation = false)
{
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
// use the `TemplateBinding` object itself as the instanced binding in order to save
// allocating a new object. If the binding *is* instantiated more than once (which can
// happen if it appears in a `Setter` for example, then just make a clone and instantiate
// that.
if (_target == null)
{
_target = (IStyledElement)target;
_targetType = targetProperty?.PropertyType;
return new InstancedBinding(
this,
Mode == BindingMode.Default ? BindingMode.OneWay : Mode,
BindingPriority.TemplatedParent);
}
else
{
var clone = new TemplateBinding
{
Converter = Converter,
ConverterParameter = ConverterParameter,
Property = Property,
};
return clone.Initiate(target, targetProperty, anchor, enableDataValidation);
}
}
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the name of the source property on the templated parent.
/// </summary>
public AvaloniaProperty Property { get; set; }
/// <inheritdoc/>
public string Description => "TemplateBinding: " + Property;
void IObserver<object>.OnCompleted() => throw new NotImplementedException();
void IObserver<object>.OnError(Exception error) => throw new NotImplementedException();
void IObserver<object>.OnNext(object value)
{
if (_target.TemplatedParent != null && Property != null)
{
if (Converter != null)
{
value = Converter.ConvertBack(
value,
Property.PropertyType,
ConverterParameter,
CultureInfo.CurrentCulture);
}
_target.TemplatedParent.SetValue(Property, value, BindingPriority.TemplatedParent);
}
}
protected override void Subscribed()
{
TemplatedParentChanged();
_target.PropertyChanged += TargetPropertyChanged;
}
protected override void Unsubscribed()
{
if (_target.TemplatedParent != null)
{
_target.TemplatedParent.PropertyChanged -= TemplatedParentPropertyChanged;
}
_target.PropertyChanged -= TargetPropertyChanged;
}
private void PublishValue()
{
if (_target.TemplatedParent != null)
{
var value = Property != null ?
_target.TemplatedParent.GetValue(Property) :
_target.TemplatedParent;
if (Converter != null)
{
value = Converter.Convert(value, _targetType, ConverterParameter, CultureInfo.CurrentCulture);
}
PublishNext(value);
}
else
{
PublishNext(AvaloniaProperty.UnsetValue);
}
}
private void TemplatedParentChanged()
{
if (_target.TemplatedParent != null)
{
_target.TemplatedParent.PropertyChanged += TemplatedParentPropertyChanged;
}
PublishValue();
}
private void TargetPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == StyledElement.TemplatedParentProperty)
{
var oldValue = (IAvaloniaObject)e.OldValue;
var newValue = (IAvaloniaObject)e.OldValue;
if (oldValue != null)
{
oldValue.PropertyChanged -= TemplatedParentPropertyChanged;
}
TemplatedParentChanged();
}
}
private void TemplatedParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Property)
{
PublishNext(_target.TemplatedParent.GetValue(Property));
}
}
}
}

2
src/OSX/Avalonia.MonoMac/KeyTransform.cs

@ -200,7 +200,7 @@ namespace Avalonia.MonoMac
[kVK_Return] = Key.Return,
[kVK_Tab] = Key.Tab,
[kVK_Space] = Key.Space,
[kVK_Delete] = Key.Delete,
[kVK_Delete] = Key.Back,
[kVK_Escape] = Key.Escape,
[kVK_Command] = Key.LWin,
[kVK_Shift] = Key.LeftShift,

3
src/Windows/Avalonia.Win32/ClipboardImpl.cs

@ -57,6 +57,9 @@ namespace Avalonia.Win32
}
await OpenClipboard();
UnmanagedMethods.EmptyClipboard();
try
{
var hGlobal = Marshal.StringToHGlobalUni(text);

15
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@ -337,6 +337,21 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public void Second_Subscription_Should_Fire_Immediately()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
object result = null;
target.Subscribe();
target.Subscribe(x => result = x);
Assert.Equal("foo", result);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase
{
private string _stringValue;

5
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

@ -4,7 +4,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins;
using Xunit;
@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
var sub = validator.Subscribe(_ => { });
validator.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
sub.Dispose();
validator.Unsubscribe();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
}

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

@ -167,7 +167,7 @@ namespace Avalonia.Base.UnitTests
target.Add(Single("foo"), 0);
owner.Verify(x => x.Changed(target, AvaloniaProperty.UnsetValue, "foo"));
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo"));
}
[Fact]
@ -180,7 +180,7 @@ namespace Avalonia.Base.UnitTests
target.Add(subject, 0);
subject.OnNext("bar");
owner.Verify(x => x.Changed(target, "foo", "bar"));
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar"));
}
[Fact]

117
tests/Avalonia.Controls.UnitTests/ApplicationTests.cs

@ -0,0 +1,117 @@
// 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 Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ApplicationTests
{
[Fact]
public void Should_Exit_After_MainWindow_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnMainWindowClose;
var mainWindow = new Window();
mainWindow.Show();
Application.Current.MainWindow = mainWindow;
var window = new Window();
window.Show();
mainWindow.Close();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Exit_After_Last_Window_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnLastWindowClose;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
windowA.Close();
Assert.False(Application.Current.IsExiting);
windowB.Close();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Only_Exit_On_Explicit_Exit()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnExplicitExit;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
windowA.Close();
Assert.False(Application.Current.IsExiting);
windowB.Close();
Assert.False(Application.Current.IsExiting);
Application.Current.Exit();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
foreach (var window in windows)
{
window.Show();
}
Application.Current.Exit();
Assert.Empty(Application.Current.Windows);
}
}
[Fact]
public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Assert.Throws<ArgumentNullException>(() => { Application.Current.Run(null); });
}
}
}
}

20
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -315,6 +315,26 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(before, after);
}
[Fact]
public void Should_Clear_Containers_When_ItemsPresenter_Changes()
{
var target = new ItemsControl
{
Items = new[] { "foo", "bar" },
Template = GetTemplate(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(2, target.ItemContainerGenerator.Containers.Count());
target.Template = GetTemplate();
target.ApplyTemplate();
Assert.Empty(target.ItemContainerGenerator.Containers);
}
[Fact]
public void Empty_Class_Should_Initially_Be_Applied()
{

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

@ -160,6 +160,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(target, child.GetLogicalParent());
}
[Fact]
public void Changing_Template_Should_Clear_Old_Templated_Childs_Parent()
{
var target = new TemplatedControl
{
Template = new FuncControlTemplate(_ => new Decorator())
};
target.ApplyTemplate();
var child = (Decorator)target.GetVisualChildren().Single();
target.Template = new FuncControlTemplate(_ => new Canvas());
target.ApplyTemplate();
Assert.Null(child.Parent);
}
[Fact]
public void Nested_Templated_Control_Should_Not_Have_Template_Applied()
{

31
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests
window.Show();
Assert.Equal(new[] { window }, Window.OpenWindows);
Assert.Equal(new[] { window }, Application.Current.Windows);
}
}
@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests
window.Show();
window.IsVisible = true;
Assert.Equal(new[] { window }, Window.OpenWindows);
Assert.Equal(new[] { window }, Application.Current.Windows);
window.Close();
}
@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests
window.Show();
window.Close();
Assert.Empty(Window.OpenWindows);
Assert.Empty(Application.Current.Windows);
}
}
@ -184,7 +184,28 @@ namespace Avalonia.Controls.UnitTests
window.Show();
windowImpl.Object.Closed();
Assert.Empty(Window.OpenWindows);
Assert.Empty(Application.Current.Windows);
}
}
[Fact]
public void Closing_Should_Only_Be_Invoked_Once()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var count = 0;
window.Closing +=
(sender, e) =>
{
count++;
};
window.Show();
window.Close();
Assert.Equal(1, count);
}
}
@ -339,7 +360,7 @@ namespace Avalonia.Controls.UnitTests
{
// HACK: We really need a decent way to have "statics" that can be scoped to
// AvaloniaLocator scopes.
((IList<Window>)Window.OpenWindows).Clear();
Application.Current.Windows.Clear();
}
}
}

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

@ -8,7 +8,6 @@ using Avalonia.Markup.Xaml.Converters;
using Avalonia.Styling;
using Xunit;
using System.ComponentModel;
using Portable.Xaml.ComponentModel;
using Portable.Xaml;
using Portable.Xaml.Markup;
@ -53,7 +52,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void ConvertFrom_Finds_Attached_Property_With_Parentheses()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var result = target.ConvertFrom(context, null, "(AttachedOwner.Attached)");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
private ITypeDescriptorContext CreateContext(Style style = null)
{
var tdMock = new Mock<ITypeDescriptorContext>();

14
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs

@ -1,18 +1,12 @@
// 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.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Moq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using Xunit;
using System.Reactive.Disposables;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using System.Linq;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@ -30,7 +24,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
<Button Name='button'>
<Button.Template>
<ControlTemplate>
<TextBlock Text='{TemplateBinding}'/>
<TextBlock Tag='{TemplateBinding}'/>
</ControlTemplate>
</Button.Template>
</Button>
@ -43,7 +37,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
button.ApplyTemplate();
var textBlock = (TextBlock)button.GetVisualChildren().Single();
Assert.Equal("Avalonia.Controls.Button", textBlock.Text);
Assert.Same(button, textBlock.Tag);
}
}
}

2
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -686,7 +686,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Template>
<ControlTemplate>
<ControlTemplate TargetType='Window'>
<ContentPresenter Name='PART_ContentPresenter'
Content='{TemplateBinding Content}'/>
</ControlTemplate>

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

@ -54,15 +54,16 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void Should_Complete_When_Activator_Completes()
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(_ => { }, () => completed = true);
activator.OnCompleted();
target.Subscribe(_ => { }, x => completed = true);
source.OnError(error);
Assert.True(completed);
}

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

@ -17,6 +17,7 @@ namespace Avalonia.Styling.UnitTests
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);
@ -36,6 +37,7 @@ namespace Avalonia.Styling.UnitTests
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
activator.OnCompleted();
Assert.True(source.Completed);
@ -47,10 +49,14 @@ namespace Avalonia.Styling.UnitTests
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();
activator.OnError(new Exception());
target.Subscribe(_ => { }, e => targetError = e);
activator.OnError(error);
Assert.NotNull(source.Error);
Assert.Same(error, source.Error);
Assert.Same(error, targetError);
}
private class TestSubject : ISubject<object>

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

@ -40,6 +40,20 @@ namespace Avalonia.Styling.UnitTests
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()
{

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

@ -1,6 +1,7 @@
// 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.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
@ -8,6 +9,7 @@ using Moq;
using Avalonia.Controls;
using Avalonia.Styling;
using Xunit;
using System.Collections.Generic;
namespace Avalonia.Styling.UnitTests
{
@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests
Assert.False(await activator.Take(1));
}
[Fact]
public void Only_Notifies_When_Result_Changes()
{
// Test for #1698
var control = new Control1
{
Classes = new Classes { "foo" },
};
var target = default(Selector).Class("foo");
var activator = target.Match(control).ObservableResult;
var result = new List<bool>();
using (activator.Subscribe(x => result.Add(x)))
{
control.Classes.Add("bar");
control.Classes.Remove("foo");
}
Assert.Equal(new[] { true, false }, result);
}
public class Control1 : TestControlBase
{
}

29
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -7,6 +7,7 @@ using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
using System.Globalization;
using System.IO;
public class PathMarkupParserTests
@ -69,7 +70,7 @@ namespace Avalonia.Visuals.UnitTests.Media
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("F 1M0,0");
parser.Parse("F 1M0,0");
Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
}
@ -139,9 +140,33 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new Point(30, 30), lineSegment.Point);
}
}
}
[Fact]
public void Parses_Scientific_Notation_Double()
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("M -1.01725E-005 -1.01725e-005");
var figure = pathGeometry.Figures[0];
Assert.Equal(
new Point(
double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture)),
figure.StartPoint);
}
}
[Theory]
[InlineData("M5.5.5 5.5.5 5.5.5")]
[InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 "
+ "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 "
+ "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 "
+ "15.9201,11L16.0001,16 9.0001,16 9.0001,12.562 9.0001,11z")] // issue #1708
[InlineData(" M0 0")]
[InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
[InlineData("M0 0L10 10z")]

Loading…
Cancel
Save