diff --git a/.editorconfig b/.editorconfig index 64fe33bbae..b7a03207a4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,159 @@ -; This file is for unifying the coding style for different editors and IDEs. -; More information at http://EditorConfig.org +# editorconfig.org +# top-most EditorConfig file root = true +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation [*] -end_of_line = CRLF +insert_final_newline = true +indent_style = space +indent_size = 4 +# C# files [*.cs] -indent_style = space +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# prefer var +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true:suggestion + +# use language keywords instead of BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style + +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal + +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# use accessibility modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# Code style defaults +dotnet_sort_system_directives_first = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Xaml files +[*.xaml] indent_size = 4 + +# Xml project files +[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf diff --git a/.gitignore b/.gitignore index a9a8fd36b4..32acee4c90 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,11 @@ $RECYCLE.BIN/ ################# .idea +################# +## VS Code +################# +.vscode/ + ################# ## Cake ################# @@ -176,5 +181,9 @@ nuget Avalonia.XBuild.sln project.lock.json .idea/* -**/obj-Skia/* -**/obj-Direct2D1/* + + +################## +## BenchmarkDotNet +################## +BenchmarkDotNet.Artifacts/ diff --git a/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.net461.v3.ncrunchproject b/.ncrunch/BindingDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Previewer.v3.ncrunchproject b/.ncrunch/Previewer.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Previewer.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RemoteDemo.v3.ncrunchproject b/.ncrunch/RemoteDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RemoteDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.net461.v3.ncrunchproject b/.ncrunch/RenderDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 39396f3ab8..d1c5026e58 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -58,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9B9E EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig src\Shared\SharedAssemblyInfo.cs = src\Shared\SharedAssemblyInfo.cs EndProjectSection EndProject @@ -398,6 +399,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 +409,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 diff --git a/build.cake b/build.cake index 561a33186a..78c166a3bc 100644 --- a/build.cake +++ b/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((context, buildContext) => // TASKS /////////////////////////////////////////////////////////////////////////////// -Task("Clean") +Task("Clean-Impl") .Does(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((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(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(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((context, data) => !data.Parameters.SkipTests) - .Does(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(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((context, data) => !data.Parameters.SkipTests) - .Does(data => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); +}); -Task("Run-Render-Tests") - .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .Does(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((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) + .Does(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((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .IsDependentOn("Build") +Task("Run-Leak-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((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(data => { CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot); }); -Task("Zip-Files") - .IsDependentOn("Copy-Files") +Task("Zip-Files-Impl") .Does(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(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((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((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((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((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((context, data) => data.Parameters.IsRunningOnWindows) - .IsDependentOn("Restore-NuGet-Packages") + .WithCriteria((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"); diff --git a/build/System.Memory.props b/build/System.Memory.props new file mode 100644 index 0000000000..f3253f8882 --- /dev/null +++ b/build/System.Memory.props @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.cake b/packages.cake index 7e7e722c82..51d9c48159 100644 --- a/packages.cake +++ b/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>> PackageVersions{get; private set;} - - - + class DependencyBuilder : List { 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 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(); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop); - NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + + if (!parameters.IsPlatformNetCoreOnly) { + NuspecNuGetSettings.Add(nuspecNuGetSettingInterop); + NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + } NuspecNuGetSettings.ForEach((nuspec) => SetNuGetNuspecCommonProperties(nuspec)); diff --git a/parameters.cake b/parameters.cake index ffd472cbd4..e595b8159f 100644 --- a/parameters.cake +++ b/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); } diff --git a/readme.md b/readme.md index 4fe76a2faf..345ad7fe9b 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,8 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). + Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) Use these commands in Package Manager console to install Avalonia manually: diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a69fb75742..95713dc22f 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -18,18 +18,18 @@ - + - + - + !BooleanString @@ -37,13 +37,13 @@ - + - + @@ -52,7 +52,7 @@ - + @@ -68,11 +68,11 @@ - + - + @@ -87,16 +87,16 @@ - + - + - + @@ -104,7 +104,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml index 1797fb48bc..fba15f6e77 100644 --- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -1,11 +1,11 @@  - + ButtonSpinner The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. - + AllowSpin ShowButtonSpinner - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml index a433fd1add..c47fd766fb 100644 --- a/samples/ControlCatalog/Pages/CalendarPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -1,13 +1,13 @@ - + Calendar A calendar control for selecting dates + Spacing="16"> - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index f934f57c22..10a38895a2 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -1,5 +1,5 @@ - + Canvas A panel which lays out its children by explicit coordinates @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 3468b71fd8..cf9b13c00c 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -1,9 +1,9 @@ - + Carousel An items control that displays its items as pages that fill the control. - + @@ -20,7 +20,7 @@ - + Transition None @@ -29,7 +29,7 @@ - + Orientation Horizontal @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index a00b3a7bef..154a6254a4 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -1,15 +1,15 @@ - + CheckBox A check box control + Spacing="16"> + Spacing="16"> Unchecked Checked Indeterminate @@ -17,7 +17,7 @@ + Spacing="16"> Three State: Unchecked Three State: Checked Three State: Indeterminate @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 3af823befc..37eeaeb2ac 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -1,12 +1,12 @@ - + Context Menu A right click menu that can be applied to any control. + Spacing="16"> @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/DatePickerPage.xaml index 92cfa7e178..2c34460fce 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml @@ -1,13 +1,13 @@ - + DatePicker A control for selecting dates with a calendar drop-down + Spacing="16"> @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index c3e9435630..710d791f3a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,5 +1,5 @@ - + @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index af679d2f9a..1f3cd3ff71 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -1,12 +1,12 @@ - + Drag+Drop Example of Drag+Drop capabilities + Spacing="16"> Drag Me @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DropDownPage.xaml b/samples/ControlCatalog/Pages/DropDownPage.xaml index 0a7a88e331..5e2a3102e7 100644 --- a/samples/ControlCatalog/Pages/DropDownPage.xaml +++ b/samples/ControlCatalog/Pages/DropDownPage.xaml @@ -1,9 +1,9 @@ - + DropDown A drop-down list. - + Inline Items Inline Item 2 @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml index e32fa1caf1..91440929f5 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml @@ -1,12 +1,12 @@ - + Expander Expands to show nested content + Spacing="16"> Expanded content @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index dc93808f27..78fbf90192 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,12 +1,12 @@ - + Image Displays an image + Spacing="16"> No Stretch - + Menu A window menu + Spacing="16"> diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index a5c911f47d..305bcd177c 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -1,6 +1,6 @@  - + Numeric up-down control Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. @@ -26,7 +26,7 @@ VerticalAlignment="Center" Margin="2"> - + @@ -69,7 +69,7 @@ - + Usage of NumericUpDown: - + ProgressBar A progress bar control @@ -7,8 +7,8 @@ - + Spacing="16"> + diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index d382b94f2c..0882817a9a 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -1,22 +1,22 @@ - + RadioButton Allows the selection of a single option of many + Spacing="16"> + Spacing="16"> Option 1 Option 2 Option 3 Disabled + Spacing="16"> Three States: Option 1 Three States: Option 2 Three States: Option 3 diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index e43968cb8e..6db71b5fcc 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -1,9 +1,9 @@ - + Slider A control that lets the user select from a range of values by moving a Thumb control along a Track. - + - + TextBox A control into which the user can input text - + Spacing="16"> + + - + - + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index aa7d60bd11..ad832b9b82 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -1,6 +1,6 @@ + Spacing="4"> ToolTip A control which pops up a hint when a control is hovered diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 5806e58c27..1ab49dbb30 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -1,12 +1,12 @@ - + TreeView Displays a hierachical tree of data. + Spacing="16"> diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index e5baf9e942..7d72d1821b 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -8,7 +8,7 @@ + SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"> @@ -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"/> diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index 29e5d854f9..b5f8ccaf01 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -9,7 +9,7 @@ + SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"> @@ -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"/> diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index eb94253d27..730b61ed54 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -6,7 +6,7 @@ + Spacing="4"> /// Tracks the progress of an animation. /// - public class Animation : AvaloniaList, IDisposable, IAnimation + public class Animation : AvaloniaList, IAnimation { private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { @@ -24,7 +27,7 @@ namespace Avalonia.Animation }; public static void RegisterAnimator(Func condition) - where TAnimator: IAnimator + where TAnimator : IAnimator { Animators.Insert(0, (condition, typeof(TAnimator))); } @@ -41,8 +44,6 @@ namespace Avalonia.Animation return null; } - private bool _isChildrenChanged = false; - private List _subscription = new List(); public AvaloniaList _animators { get; set; } = new AvaloniaList(); /// @@ -68,22 +69,18 @@ namespace Avalonia.Animation /// /// The value fill mode for this animation. /// - public FillMode FillMode { get; set; } + public FillMode FillMode { get; set; } /// /// Easing function to be used. - /// + /// public Easing Easing { get; set; } = new LinearEasing(); - public Animation() - { - this.CollectionChanged += delegate { _isChildrenChanged = true; }; - } - - private void InterpretKeyframes() + private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { - var handlerList = new List<(Type, AvaloniaProperty)>(); - var kfList = new List(); + var handlerList = new List<(Type type, AvaloniaProperty property)>(); + var animatorKeyFrames = new List(); + var subscriptions = new List(); foreach (var keyframe in this) { @@ -99,68 +96,87 @@ namespace Avalonia.Animation if (!handlerList.Contains((handler, setter.Property))) handlerList.Add((handler, setter.Property)); - var newKF = new AnimatorKeyFrame() + var cue = keyframe.Cue; + + if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) { - Handler = handler, - Property = setter.Property, - Cue = keyframe.Cue, - KeyTime = keyframe.KeyTime, - timeSpanSet = keyframe.timeSpanSet, - cueSet = keyframe.cueSet, - Value = setter.Value - }; - - kfList.Add(newKF); + cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + } + + var newKF = new AnimatorKeyFrame(handler, cue); + + subscriptions.Add(newKF.BindSetter(setter, control)); + + animatorKeyFrames.Add(newKF); } } - var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); + var newAnimatorInstances = new List(); - foreach (var handler in handlerList) + foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1); - newInstance.Property = handler.Item2; - newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + newInstance.Property = property; + newAnimatorInstances.Add(newInstance); } - foreach (var kf in kfList) + foreach (var keyframe in animatorKeyFrames) { - var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && - p.prop == kf.Property) - .First(); - parent.inst.Add(kf); + var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType && + a.Property == keyframe.Property); + animator.Add(keyframe); } - foreach(var instance in newAnimatorInstances) - _animators.Add(instance.inst); - + return (newAnimatorInstances, subscriptions); } - /// - /// Cancels the animation. - /// - public void Dispose() + /// + public IDisposable Apply(Animatable control, IObservable match, Action onComplete) { - foreach (var sub in _subscription) + var (animators, subscriptions) = InterpretKeyframes(control); + if (animators.Count == 1) + { + subscriptions.Add(animators[0].Apply(this, control, match, onComplete)); + } + else { - sub.Dispose(); + var completionTasks = onComplete != null ? new List() : null; + foreach (IAnimator animator in animators) + { + Action animatorOnComplete = null; + if (onComplete != null) + { + var tcs = new TaskCompletionSource(); + animatorOnComplete = () => tcs.SetResult(null); + completionTasks.Add(tcs.Task); + } + subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete)); + } + + if (onComplete != null) + { + Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); + } } + return new CompositeDisposable(subscriptions); } /// - public IDisposable Apply(Animatable control, IObservable matchObs) + public Task RunAsync(Animatable control) { - if (_isChildrenChanged) - { - InterpretKeyframes(); - _isChildrenChanged = false; - } + var run = new TaskCompletionSource(); - foreach (IAnimator keyframes in _animators) + if (this.RepeatCount == RepeatCount.Loop) + run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); + + IDisposable subscriptions = null; + subscriptions = this.Apply(control, Observable.Return(true), () => { - _subscription.Add(keyframes.Apply(this, control, matchObs)); - } - return this; + run.SetResult(null); + subscriptions?.Dispose(); + }); + + return run.Task; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 02457cb9aa..0276c6fa92 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -4,6 +4,8 @@ using System.Text; using System.ComponentModel; using Avalonia.Metadata; using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Animation { @@ -11,13 +13,63 @@ namespace Avalonia.Animation /// Defines a KeyFrame that is used for /// objects. /// - public class AnimatorKeyFrame + public class AnimatorKeyFrame : AvaloniaObject { - public Type Handler; - public Cue Cue; - public TimeSpan KeyTime; - internal bool timeSpanSet, cueSet; - public AvaloniaProperty Property; - public object Value; + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); + + public AnimatorKeyFrame() + { + + } + + public AnimatorKeyFrame(Type animatorType, Cue cue) + { + AnimatorType = animatorType; + Cue = cue; + } + + public Type AnimatorType { get; } + public Cue Cue { get; } + public AvaloniaProperty Property { get; private set; } + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl) + { + Property = setter.Property; + var value = setter.Value; + + if (value is IBinding binding) + { + return this.Bind(ValueProperty, binding, targetControl); + } + else + { + return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl); + } + } + + public T GetTypedValue() + { + var typeConv = TypeDescriptor.GetConverter(typeof(T)); + + if (Value == null) + { + throw new ArgumentNullException($"KeyFrame value can't be null."); + } + if (!typeConv.CanConvertTo(Value.GetType())) + { + throw new InvalidCastException($"KeyFrame value doesnt match property type."); + } + + return (T)typeConv.ConvertTo(Value, typeof(T)); + } } } diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index e37b0e592a..87e189c997 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -35,6 +35,7 @@ namespace Avalonia.Animation private T _neutralValue; internal bool _unsubscribe = false; private IObserver _targetObserver; + private readonly Action _onComplete; [Flags] private enum KeyFramesStates @@ -51,9 +52,9 @@ namespace Avalonia.Animation Disposed } - public void Initialize(Animation animation, Animatable control, Animator keyframes) + public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action onComplete) { - _parent = keyframes; + _parent = animator; _targetAnimation = animation; _targetControl = control; _neutralValue = (T)_targetControl.GetValue(_parent.Property); @@ -82,6 +83,7 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.DoDelay; else _currentState = KeyFramesStates.DoRun; + _onComplete = onComplete; } public void Step(PlayState _playState, Func Interpolator) @@ -123,121 +125,136 @@ namespace Avalonia.Animation double _tempDuration = 0d, _easedTime; - checkstate: - switch (_currentState) + bool handled = false; + + while (!handled) { - case KeyFramesStates.DoDelay: + switch (_currentState) + { + case KeyFramesStates.DoDelay: - if (_fillMode == FillMode.Backward - || _fillMode == FillMode.Both) - { - if (_currentIteration == 0) + if (_fillMode == FillMode.Backward + || _fillMode == FillMode.Both) { - _targetObserver.OnNext(_firstKFValue); + if (_currentIteration == 0) + { + _targetObserver.OnNext(_firstKFValue); + } + else + { + _targetObserver.OnNext(_lastInterpValue); + } + } + + if (_delayFrameCount > _delayTotalFrameCount) + { + _currentState = KeyFramesStates.DoRun; } else { - _targetObserver.OnNext(_lastInterpValue); + handled = true; + _delayFrameCount++; } - } - - if (_delayFrameCount > _delayTotalFrameCount) - { - _currentState = KeyFramesStates.DoRun; - goto checkstate; - } - _delayFrameCount++; - break; - - case KeyFramesStates.DoRun: - - if (_isReversed) - _currentState = KeyFramesStates.RunBackwards; - else - _currentState = KeyFramesStates.RunForwards; - - goto checkstate; - - case KeyFramesStates.RunForwards: - - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + break; - _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + case KeyFramesStates.DoRun: - goto checkstate; + if (_isReversed) + _currentState = KeyFramesStates.RunBackwards; + else + _currentState = KeyFramesStates.RunForwards; - case KeyFramesStates.RunBackwards: + break; - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + case KeyFramesStates.RunForwards: - _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; - goto checkstate; + } + break; - case KeyFramesStates.RunApplyValue: + case KeyFramesStates.RunBackwards: - _easedTime = _targetAnimation.Easing.Ease(_tempDuration); + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; + } + break; - _durationFrameCount++; - _lastInterpValue = Interpolator(_easedTime, _neutralValue); - _targetObserver.OnNext(_lastInterpValue); - _currentState = KeyFramesStates.DoRun; + case KeyFramesStates.RunApplyValue: - break; + _easedTime = _targetAnimation.Easing.Ease(_tempDuration); - case KeyFramesStates.RunComplete: + _durationFrameCount++; + _lastInterpValue = Interpolator(_easedTime, _neutralValue); + _targetObserver.OnNext(_lastInterpValue); + _currentState = KeyFramesStates.DoRun; + handled = true; + break; - if (_checkLoopAndRepeat) - { - _delayFrameCount = 0; - _durationFrameCount = 0; + case KeyFramesStates.RunComplete: - if (_isLooping) - { - _currentState = KeyFramesStates.DoRun; - } - else if (_isRepeating) + if (_checkLoopAndRepeat) { - if (_currentIteration >= _repeatCount) + _delayFrameCount = 0; + _durationFrameCount = 0; + + if (_isLooping) { - _currentState = KeyFramesStates.Stop; + _currentState = KeyFramesStates.DoRun; } - else + else if (_isRepeating) { - _currentState = KeyFramesStates.DoRun; + if (_currentIteration >= _repeatCount) + { + _currentState = KeyFramesStates.Stop; + } + else + { + _currentState = KeyFramesStates.DoRun; + } + _currentIteration++; } - _currentIteration++; - } - if (_animationDirection == PlaybackDirection.Alternate - || _animationDirection == PlaybackDirection.AlternateReverse) - _isReversed = !_isReversed; + if (_animationDirection == PlaybackDirection.Alternate + || _animationDirection == PlaybackDirection.AlternateReverse) + _isReversed = !_isReversed; + + break; + } + _currentState = KeyFramesStates.Stop; break; - } - _currentState = KeyFramesStates.Stop; - goto checkstate; + case KeyFramesStates.Stop: - case KeyFramesStates.Stop: + if (_fillMode == FillMode.Forward + || _fillMode == FillMode.Both) + { + _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + } - if (_fillMode == FillMode.Forward - || _fillMode == FillMode.Both) - { - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); - } - _targetObserver.OnCompleted(); - break; + _targetObserver.OnCompleted(); + _onComplete?.Invoke(); + Dispose(); + handled = true; + break; + default: + handled = true; + break; + } } } @@ -253,4 +270,4 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.Disposed; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 6d4ae7d8e2..eb8b40647d 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private Dictionary _convertedKeyframes = new Dictionary(); + private readonly SortedList _convertedKeyframes = new SortedList(); private bool _isVerfifiedAndConverted; @@ -35,18 +35,17 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) { if (!_isVerfifiedAndConverted) - VerifyConvertKeyFrames(animation, typeof(T)); + VerifyConvertKeyFrames(); return obsMatch - .Where(p => p == true) // Ignore triggers when global timers are paused. - .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) + .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) .Subscribe(_ => { - var timerObs = RunKeyFrames(animation, control); + var timerObs = RunKeyFrames(animation, control, onComplete); }); } @@ -60,8 +59,8 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - KeyValuePair firstCue, lastCue; - int kvCount = _convertedKeyframes.Count(); + KeyValuePair firstCue, lastCue; + int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) @@ -76,8 +75,8 @@ namespace Avalonia.Animation } else { - firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); - lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); + firstCue = _convertedKeyframes.Last(j => j.Key <= t); + lastCue = _convertedKeyframes.First(j => j.Key >= t); } } else @@ -89,26 +88,24 @@ namespace Avalonia.Animation double t0 = firstCue.Key; double t1 = lastCue.Key; var intraframeTime = (t - t0) / (t1 - t0); - return (intraframeTime, new KeyFramePair(firstCue, lastCue)); + var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); + var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); + return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } /// /// Runs the KeyFrames Animation. /// - private IDisposable RunKeyFrames(Animation animation, Animatable control) + private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) { - var _kfStateMach = new AnimatorStateMachine(); - _kfStateMach.Initialize(animation, control, this); + var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); Timing.AnimationStateTimer - .TakeWhile(_ => !_kfStateMach._unsubscribe) - .Subscribe(p => - { - _kfStateMach.Step(p, DoInterpolation); - }); + .TakeWhile(_ => !stateMachine._unsubscribe) + .Subscribe(p => stateMachine.Step(p, DoInterpolation)); - return control.Bind(Property, _kfStateMach, BindingPriority.Animation); + return control.Bind(Property, stateMachine, BindingPriority.Animation); } /// @@ -119,39 +116,19 @@ namespace Avalonia.Animation /// /// Verifies and converts keyframe values according to this class's target type. /// - private void VerifyConvertKeyFrames(Animation animation, Type type) + private void VerifyConvertKeyFrames() { - var typeConv = TypeDescriptor.GetConverter(type); - - foreach (AnimatorKeyFrame k in this) + foreach (AnimatorKeyFrame keyframe in this) { - if (k.Value == null) - { - throw new ArgumentNullException($"KeyFrame value can't be null."); - } - if (!typeConv.CanConvertTo(k.Value.GetType())) - { - throw new InvalidCastException($"KeyFrame value doesnt match property type."); - } - - T convertedValue = (T)typeConv.ConvertTo(k.Value, type); - - Cue _normalizedCue = k.Cue; - - if (k.timeSpanSet) - { - _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks); - } - - _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false)); + _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false)); } - SortKeyFrameCues(_convertedKeyframes); + AddNeutralKeyFramesIfNeeded(); _isVerfifiedAndConverted = true; } - private void SortKeyFrameCues(Dictionary convertedValues) + private void AddNeutralKeyFramesIfNeeded() { bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; @@ -170,23 +147,20 @@ namespace Avalonia.Animation } if (!hasStartKey || !hasEndKey) - AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); - - _convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key) - .ToDictionary((k) => k.Key, (v) => v.Value); + AddNeutralKeyFrames(hasStartKey, hasEndKey); } - private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary convertedKeyframes) + private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey) { if (!hasStartKey) { - convertedKeyframes.Add(0.0d, (default(T), true)); + _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } if (!hasEndKey) { - convertedKeyframes.Add(1.0d, (default(T), true)); + _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index fe36b13495..5a95c108e3 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// A Cue object for . /// [TypeConverter(typeof(CueTypeConverter))] - public struct Cue : IEquatable, IEquatable + public readonly struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 diff --git a/src/Avalonia.Animation/DoubleAnimator.cs b/src/Avalonia.Animation/DoubleAnimator.cs index 5b994377f1..154f37360c 100644 --- a/src/Avalonia.Animation/DoubleAnimator.cs +++ b/src/Avalonia.Animation/DoubleAnimator.cs @@ -24,15 +24,15 @@ namespace Avalonia.Animation var firstKF = pair.KFPair.FirstKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame; - if (firstKF.Value.isNeutral) + if (firstKF.isNeutral) y0 = neutralValue; else - y0 = firstKF.Value.TargetValue; + y0 = firstKF.TargetValue; - if (secondKF.Value.isNeutral) + if (secondKF.isNeutral) y1 = neutralValue; else - y1 = secondKF.Value.TargetValue; + y1 = secondKF.TargetValue; // Do linear parametric interpolation return y0 + (pair.IntraKFTime) * (y1 - y0); diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 4de7e46af5..905d90fa52 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace Avalonia.Animation { @@ -12,6 +13,11 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control /// - IDisposable Apply(Animatable control, IObservable match); + IDisposable Apply(Animatable control, IObservable match, Action onComplete = null); + + /// + /// Run the animation to the specified control + /// + Task RunAsync(Animatable control); } } diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index f2a94c9ed6..2d22377286 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -5,4 +5,4 @@ namespace Avalonia.Animation AvaloniaProperty Property { get; set; } object Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 6acca4d697..8b763db603 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -17,6 +17,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch); + IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete); } } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index 46be119c36..ea04aa0aab 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -7,6 +7,11 @@ using Avalonia.Collections; namespace Avalonia.Animation { + internal enum KeyFrameTimingMode + { + TimeSpan = 1, + Cue + } /// /// Stores data regarding a specific key @@ -14,7 +19,6 @@ namespace Avalonia.Animation /// public class KeyFrame : AvaloniaList { - internal bool timeSpanSet, cueSet; private TimeSpan _ktimeSpan; private Cue _kCue; @@ -30,6 +34,8 @@ namespace Avalonia.Animation { } + internal KeyFrameTimingMode TimingMode { get; private set; } + /// /// Gets or sets the key time of this . /// @@ -42,11 +48,11 @@ namespace Avalonia.Animation } set { - if (cueSet) + if (TimingMode == KeyFrameTimingMode.Cue) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - timeSpanSet = true; + TimingMode = KeyFrameTimingMode.TimeSpan; _ktimeSpan = value; } } @@ -63,11 +69,11 @@ namespace Avalonia.Animation } set { - if (timeSpanSet) + if (TimingMode == KeyFrameTimingMode.TimeSpan) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - cueSet = true; + TimingMode = KeyFrameTimingMode.Cue; _kCue = value; } } diff --git a/src/Avalonia.Animation/KeyFramePair`1.cs b/src/Avalonia.Animation/KeyFramePair`1.cs index c192479a1d..408b13e0d8 100644 --- a/src/Avalonia.Animation/KeyFramePair`1.cs +++ b/src/Avalonia.Animation/KeyFramePair`1.cs @@ -22,7 +22,7 @@ namespace Avalonia.Animation /// /// /// - public KeyFramePair(KeyValuePair FirstKeyFrame, KeyValuePair LastKeyFrame) : this() + public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this() { this.FirstKeyFrame = FirstKeyFrame; this.SecondKeyFrame = LastKeyFrame; @@ -31,11 +31,11 @@ namespace Avalonia.Animation /// /// First object. /// - public KeyValuePair FirstKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) FirstKeyFrame { get; } /// /// Second object. /// - public KeyValuePair SecondKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) SecondKeyFrame { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48e72db126..35e189e6a4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/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; @@ -21,24 +22,18 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { /// /// The parent object that inherited values are inherited from. /// private IAvaloniaObject _inheritanceParent; - /// - /// The set values/bindings on this object. - /// - private readonly Dictionary _values = - new Dictionary(); - /// /// Maintains a list of direct property binding subscriptions so that the binding source /// doesn't get collected. /// - private List _directBindings; + private List _directBindings; /// /// Event handler for implementation. @@ -50,20 +45,8 @@ namespace Avalonia /// private EventHandler _propertyChanged; - private DeferredSetter _directDeferredSetter; - - /// - /// Delayed setter helper for direct properties. Used to fix #855. - /// - private DeferredSetter DirectPropertyDeferredSetter - { - get - { - return _directDeferredSetter ?? - (_directDeferredSetter = new DeferredSetter()); - } - } - + private ValueStore _values; + private ValueStore Values => _values ?? (_values = new ValueStore(this)); /// /// Initializes a new instance of the class. @@ -227,9 +210,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 +250,7 @@ namespace Avalonia Contract.Requires(property != null); VerifyAccess(); - return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + return _values?.IsAnimating(property) ?? false; } /// @@ -273,14 +267,7 @@ namespace Avalonia Contract.Requires(property != null); VerifyAccess(); - PriorityValue value; - - if (_values.TryGetValue(property, out value)) - { - return value.Value != AvaloniaProperty.UnsetValue; - } - - return false; + return _values?.IsSet(property) ?? false; } /// @@ -359,36 +346,15 @@ namespace Avalonia property, description); - IDisposable subscription = null; - if (_directBindings == null) { - _directBindings = new List(); + _directBindings = new List(); } - 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 +363,7 @@ namespace Avalonia description, priority); - return v.Add(source, (int)priority); + return Values.AddBinding(property, source, priority); } } @@ -428,20 +394,11 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { VerifyAccess(); - PriorityValue value; - - if (_values.TryGetValue(property, out value)) - { - value.Revalidate(); - } + _values?.Revalidate(property); } - - /// - void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue) + + internal void PriorityValueChanged(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 +408,7 @@ namespace Avalonia if (!Equals(oldValue, newValue)) { - RaisePropertyChanged(property, oldValue, newValue, priority); + RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); Logger.Verbose( LogArea.Property, @@ -460,14 +417,13 @@ namespace Avalonia property, oldValue, newValue, - priority); + (BindingPriority)priority); } } - - /// - void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification) + + internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { - UpdateDataValidation(sender.Property, notification); + UpdateDataValidation(property, notification); } /// @@ -480,10 +436,7 @@ namespace Avalonia /// Gets all priority values set on the object. /// /// A collection of property/value tuples. - internal IDictionary GetSetValues() - { - return _values; - } + internal IDictionary GetSetValues() => Values?.GetSetValues(); /// /// Forces revalidation of properties when a property value changes. @@ -593,12 +546,12 @@ namespace Avalonia T value) { Contract.Requires(setterCallback != null); - return DirectPropertyDeferredSetter.SetAndNotify( + return Values.Setter.SetAndNotify( property, ref field, - (object val, ref T backing, Action notify) => + (object update, ref T backing, Action notify) => { - setterCallback((T)val, ref backing, notify); + setterCallback((T)update, ref backing, notify); return true; }, value); @@ -672,68 +625,18 @@ namespace Avalonia } } - /// - /// Creates a for a . - /// - /// The property. - /// The . - private PriorityValue CreatePriorityValue(AvaloniaProperty property) - { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType()); - Func validate2 = null; - - if (validate != null) - { - validate2 = v => validate(this, v); - } - - PriorityValue result = new PriorityValue( - this, - property, - property.PropertyType, - validate2); - - return result; - } - /// /// Gets the default value for a property. /// /// The property. /// The default value. - 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()); } - /// - /// Gets a 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 - /// - /// The property. - /// The value. - 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; - } - /// /// Sets the value of a direct property. /// @@ -814,21 +717,8 @@ namespace Avalonia originalValue?.GetType().FullName ?? "(null)")); } - PriorityValue v; - - if (!_values.TryGetValue(property, out v)) - { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } - - v = CreatePriorityValue(property); - _values.Add(property, v); - } - LogPropertySet(property, value, priority); - v.SetValue(value, (int)priority); + Values.AddValue(property, value, (int)priority); } /// @@ -908,5 +798,38 @@ namespace Avalonia value, priority); } + + private class DirectBindingSubscription : IObserver, IDisposable + { + readonly AvaloniaObject _owner; + readonly AvaloniaProperty _property; + IDisposable _subscription; + + public DirectBindingSubscription( + AvaloniaObject owner, + AvaloniaProperty property, + IObservable 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); + } + } } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 1da2ecb942..393482cccf 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/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. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable( - observer => - { - EventHandler 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(o, property); } /// @@ -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. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return o.GetObservable((AvaloniaProperty)property).Cast(); + return new AvaloniaPropertyObservable(o, property); } /// - /// Gets an observable for a . + /// Gets an observable that listens for property changed events for an + /// . /// /// The object. - /// The type of the property. /// The property. /// - /// 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 event is raised + /// for the specified property. /// - public static IObservable> GetObservableWithHistory( + public static IObservable GetPropertyChangedObservable( this IAvaloniaObject o, - AvaloniaProperty property) + AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable>( - observer => - { - EventHandler 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); } /// @@ -166,23 +134,6 @@ namespace Avalonia o.GetObservable(property)); } - /// - /// Gets a weak observable for a . - /// - /// The object. - /// The property. - /// An observable. - public static IObservable GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property) - { - Contract.Requires(o != null); - Contract.Requires(property != null); - - return new WeakPropertyChangedObservable( - new WeakReference(o), - property, - GetDescription(o, property)); - } - /// /// Binds a property on an to an . /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index c0a4ace6ed..e29e7339ae 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -106,7 +106,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on a type by name. /// /// The type. /// The property name. @@ -130,7 +130,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on an object by name. /// /// The object. /// The property name. @@ -148,52 +148,6 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } - /// - /// Finds a registered attached property on a type by name. - /// - /// The type. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name) - { - Contract.Requires(type != null); - Contract.Requires(ownerType != null); - Contract.Requires(name != null); - - if (name.Contains('.')) - { - throw new InvalidOperationException("Attached properties not supported."); - } - - return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name); - } - - /// - /// Finds a registered non-attached property on a type by name. - /// - /// The object. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name) - { - Contract.Requires(o != null); - Contract.Requires(name != null); - - return FindRegisteredAttached(o.GetType(), ownerType, name); - } - /// /// Checks whether a is registered on a type. /// @@ -287,4 +241,4 @@ namespace Avalonia _attachedCache.Clear(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index d295cb91ce..3a355bcb48 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/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(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(e => handler.Invoke(collection, e)); + return collection.GetWeakCollectionChangedObservable() + .Subscribe(e => handler(collection, e)); } /// @@ -63,18 +58,13 @@ namespace Avalonia.Collections Contract.Requires(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(handler); + return collection.GetWeakCollectionChangedObservable().Subscribe(handler); } - private class WeakCollectionChangedObservable : ObservableBase, + private class WeakCollectionChangedObservable : LightweightObservableBase, IWeakSubscriber { private WeakReference _sourceReference; - private readonly Subject _changed = new Subject(); - - private int _count; public WeakCollectionChangedObservable(WeakReference 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 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); } } } diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs new file mode 100644 index 0000000000..f7e5de2fe2 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Reactive; + +namespace Avalonia.Data.Core +{ + public class AvaloniaPropertyAccessorNode : SettableNode + { + private IDisposable _subscription; + private readonly bool _enableValidation; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation) + { + _property = property; + _enableValidation = enableValidation; + } + + public override string Description => PropertyName; + public string PropertyName { get; } + public override Type PropertyType => _property.PropertyType; + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + if (Target.IsAlive && Target.Target is IAvaloniaObject obj) + { + obj.SetValue(_property, value, priority); + return true; + } + return false; + } + catch + { + return false; + } + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.Target is IAvaloniaObject obj) + { + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 4b41d1568c..c4ffa839e0 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/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 { /// /// 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. /// - public class BindingExpression : ISubject, IDescription + public class BindingExpression : LightweightObservableBase, ISubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; private readonly BindingPriority _priority; - private readonly Subject _errors = new Subject(); + InnerListener _innerListener; + WeakReference _value; /// /// Initializes a new instance of the 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 } } - /// - public IDisposable Subscribe(IObserver observer) + protected override void Initialize() => _innerListener = new InnerListener(this); + protected override void Deinitialize() => _innerListener.Dispose(); + + protected override void Subscribed(IObserver observer, bool first) { - return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); + if (!first && _value != null && _value.TryGetTarget(out var val) == true) + { + observer.OnNext(val); + } } + /// private object ConvertValue(object value) { var notification = value as BindingNotification; @@ -301,5 +309,28 @@ namespace Avalonia.Data.Core return a; } + + public class InnerListener : IObserver, 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(converted); + _owner.PublishNext(converted); + } + } } } diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 93e0d5947a..0e2c3c035c 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -6,13 +6,8 @@ using System.Reactive.Linq; namespace Avalonia.Data.Core { - internal class EmptyExpressionNode : ExpressionNode + public class EmptyExpressionNode : ExpressionNode { public override string Description => "."; - - protected override IObservable StartListeningCore(WeakReference reference) - { - return Observable.Return(reference.Target); - } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ac7e97a4b1..9ee4787e47 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/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 + public 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 _observer; + private Action _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 observer) + public void Subscribe(Action 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.OnCompleted() + public void Unsubscribe() { - throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); - } + Next?.Unsubscribe(); - void IObserver.OnError(Exception error) - { - throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); + if (_listening) + { + StopListening(); + } + + LastValue = null; + _subscriber = null; } - void IObserver.OnNext(object value) + protected virtual void StartListeningCore(WeakReference reference) { - NextValueChanged(value); + ValueChanged(reference.Target); } - protected virtual IObservable 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 source; - - if (target == null) - { - source = Observable.Return(TargetNullNotification()); - } - else if (target == AvaloniaProperty.UnsetValue) - { - source = Observable.Empty(); - } - 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( diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs b/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs deleted file mode 100644 index 8e9e9fc3c1..0000000000 --- a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data.Core.Parsers; - -namespace Avalonia.Data.Core -{ - internal static class ExpressionNodeBuilder - { - public static ExpressionNode Build(string expression, bool enableValidation = false) - { - if (string.IsNullOrWhiteSpace(expression)) - { - throw new ArgumentException("'expression' may not be empty."); - } - - var reader = new Reader(expression); - var parser = new ExpressionParser(enableValidation); - var node = parser.Parse(reader); - - if (!reader.End) - { - throw new ExpressionParseException(reader.Position, "Expected end of expression."); - } - - return node; - } - } -} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 14bc09f5b7..773049d3a5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -3,19 +3,20 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Data.Core { /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : ObservableBase, IDescription + public class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize @@ -54,35 +55,30 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private readonly Subject _finished; - private readonly object _root; - private IObservable _result; + private object _root; + private IDisposable _rootSubscription; + private WeakReference _value; /// /// Initializes a new instance of the class. /// /// The root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( object root, - string expression, - bool enableDataValidation = false, + ExpressionNode node, string description = null) { - Contract.Requires(expression != null); - if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } @@ -90,24 +86,19 @@ namespace Avalonia.Data.Core /// Initializes a new instance of the class. /// /// An observable which provides the root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( IObservable rootObservable, - string expression, - bool enableDataValidation = false, - string description = null) + ExpressionNode node, + string description) { Contract.Requires(rootObservable != null); - Contract.Requires(expression != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + + _node = node; + Description = description; _root = rootObservable; } @@ -115,32 +106,92 @@ namespace Avalonia.Data.Core /// Initializes a new instance of the class. /// /// A function which gets the root object. - /// The expression. + /// The expression. /// An observable which triggers a re-read of the getter. - /// Whether data validation should be enabled. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( Func rootGetter, - string expression, + ExpressionNode node, IObservable update, - bool enableDataValidation = false, - string description = null) + string description) { Contract.Requires(rootGetter != null); - Contract.Requires(expression != null); Contract.Requires(update != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); - + Description = description; + _node = node; _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } + + /// + /// Creates a new instance of the class. + /// + /// The root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + + /// + /// Creates a new instance of the class. + /// + /// An observable which provides the root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); + } + + /// + /// Creates a new instance of the class. + /// + /// A function which gets the root object. + /// The expression. + /// An observable which triggers a re-read of the getter. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + Func rootGetter, + Expression> expression, + IObservable update, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation), + update, + description ?? expression.ToString()); + } + /// /// Attempts to set the value of a property expression. /// @@ -203,77 +254,54 @@ namespace Avalonia.Data.Core } } - /// - protected override IDisposable SubscribeCore(IObserver observer) + protected override void Initialize() { - if (_result == null) - { - var source = (IObservable)_node; - - if (_finished != null) - { - source = source.TakeUntil(_finished); - } - - _result = Observable.Using(StartRoot, _ => source) - .Select(ToWeakReference) - .Publish(UninitializedValue) - .RefCount() - .Where(x => x != UninitializedValue) - .Select(Translate); - } + _value = null; + _node.Subscribe(ValueChanged); + StartRoot(); + } - return _result.Subscribe(observer); + protected override void Deinitialize() + { + _rootSubscription?.Dispose(); + _rootSubscription = null; + _node.Unsubscribe(); } - private static ExpressionNode Parse(string expression, bool enableDataValidation) + protected override void Subscribed(IObserver observer, bool first) { - if (!string.IsNullOrWhiteSpace(expression)) + if (!first && _value != null && _value.TryGetTarget(out var value)) { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); - } - else - { - return new EmptyExpressionNode(); + observer.OnNext(value); } } - private static object ToWeakReference(object o) + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - return o is BindingNotification ? o : new WeakReference(o); + return ExpressionTreeParser.Parse(expression, enableDataValidation); } - private object Translate(object o) + private void StartRoot() { - if (o is WeakReference weak) + if (_root is IObservable 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 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(value); + PublishNext(value); } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 3d7bce4080..1845b1b52a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -17,8 +17,8 @@ namespace Avalonia.Data.Core /// /// The column position of the error. /// The exception message. - public ExpressionParseException(int column, string message) - : base(message) + public ExpressionParseException(int column, string message, Exception innerException = null) + : base(message, innerException) { Column = column; } diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs new file mode 100644 index 0000000000..04412b61ef --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Data.Core +{ + class IndexerExpressionNode : IndexerNodeBase + { + private readonly ParameterExpression _parameter; + private readonly IndexExpression _expression; + private readonly Delegate _setDelegate; + private readonly Delegate _getDelegate; + private readonly Delegate _firstArgumentDelegate; + + public IndexerExpressionNode(IndexExpression expression) + { + _parameter = Expression.Parameter(expression.Object.Type); + _expression = expression.Update(_parameter, expression.Arguments); + + _getDelegate = Expression.Lambda(_expression, _parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + _setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile(); + + _firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile(); + } + + public override Type PropertyType => _expression.Type; + + public override string Description => _expression.ToString(); + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + _setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + try + { + return _getDelegate.DynamicInvoke(target); + } + catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException + || e.InnerException is IndexOutOfRangeException + || e.InnerException is KeyNotFoundException) + { + return AvaloniaProperty.UnsetValue; + } + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs new file mode 100644 index 0000000000..5c3295a9d8 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core +{ + public abstract class IndexerNodeBase : SettableNode + { + private IDisposable _subscription; + + protected override void StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var incc = target as INotifyCollectionChanged; + var inpc = target as INotifyPropertyChanged; + var inputs = new List>(); + + if (incc != null) + { + inputs.Add(WeakObservable.FromEventPattern( + incc, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + if (inpc != null) + { + inputs.Add(WeakObservable.FromEventPattern( + inpc, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription.Dispose(); + } + + protected abstract object GetValue(object target); + + protected abstract int? TryGetFirstArgumentAsInt(); + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + var index = TryGetFirstArgumentAsInt(); + + if (index == null) + { + return false; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return true; // Implementation defined meaning for the index, so just try to update anyway + } + + protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); + } +} diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index f277005cec..20f1bcd21e 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -7,7 +7,7 @@ using Avalonia.Data; namespace Avalonia.Data.Core { - internal class LogicalNotNode : ExpressionNode, ITransformNode + public class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs deleted file mode 100644 index 5c74c5cd13..0000000000 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Data.Core.Parsers -{ - internal class ExpressionParser - { - private bool _enableValidation; - - public ExpressionParser(bool enableValidation) - { - _enableValidation = enableValidation; - } - - public ExpressionNode Parse(Reader r) - { - var nodes = new List(); - var state = State.Start; - - while (!r.End && state != State.End) - { - switch (state) - { - case State.Start: - state = ParseStart(r, nodes); - break; - - case State.AfterMember: - state = ParseAfterMember(r, nodes); - break; - - case State.BeforeMember: - state = ParseBeforeMember(r, nodes); - break; - - case State.AttachedProperty: - state = ParseAttachedProperty(r, nodes); - break; - - case State.Indexer: - state = ParseIndexer(r, nodes); - break; - } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return nodes.FirstOrDefault(); - } - - private State ParseStart(Reader r, IList nodes) - { - if (ParseNot(r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - else if (ParseOpenBrace(r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(r)) - { - return State.Indexer; - } - else - { - var identifier = IdentifierParser.Parse(r); - - if (identifier != null) - { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); - return State.AfterMember; - } - } - - return State.End; - } - - private static State ParseAfterMember(Reader r, IList nodes) - { - if (ParseMemberAccessor(r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(Reader r, IList nodes) - { - if (ParseOpenBrace(r)) - { - return State.AttachedProperty; - } - else - { - var identifier = IdentifierParser.Parse(r); - - if (identifier != null) - { - nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); - return State.AfterMember; - } - - return State.End; - } - } - - private State ParseAttachedProperty(Reader r, List nodes) - { - var owner = IdentifierParser.Parse(r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = IdentifierParser.Parse(r); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(Reader r, List nodes) - { - var args = ArgumentListParser.Parse(r, '[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); - } - - nodes.Add(new IndexerNode(args)); - return State.AfterMember; - } - - private static bool ParseNot(Reader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(Reader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(Reader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(Reader r) - { - return !r.End && r.Peek == '['; - } - - private static bool ParseStreamOperator(Reader r) - { - return !r.End && r.TakeIf('^'); - } - - private enum State - { - Start, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, - } - } -} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs new file mode 100644 index 0000000000..db5d117687 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + static class ExpressionTreeParser + { + public static ExpressionNode Parse(Expression expr, bool enableDataValidation) + { + var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); + + visitor.Visit(expr); + + var nodes = visitor.Nodes; + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault() ?? new EmptyExpressionNode(); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 0000000000..1b4d1c200d --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private const string MultiDimensionalArrayGetterMethodName = "Get"; + private static PropertyInfo AvaloniaObjectIndexer; + private static MethodInfo CreateDelegateMethod; + + private readonly bool _enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + _enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) + { + Nodes.Add(new LogicalNotNode()); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand.Type.IsAssignableFrom(node.Type)) + { + // Ignore inheritance casts + } + else + { + throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression."); + } + } + else if (node.NodeType == ExpressionType.TypeAs) + { + // Ignore as operator. + } + else + { + throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression"); + } + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + var visited = base.VisitMember(node); + Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation)); + return visited; + } + + protected override Expression VisitIndex(IndexExpression node) + { + Visit(node.Object); + + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation)); + } + else + { + Nodes.Add(new IndexerExpressionNode(node)); + } + + return node; + } + + private T GetArgumentExpressionValue(Expression expr) + { + try + { + return Expression.Lambda>(expr).Compile(preferInterpretation: true)(); + } + catch (InvalidOperationException ex) + { + throw new ExpressionParseException(0, "Unable to parse indexer value.", ex); + } + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitBlock(BlockExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions."); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions."); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression."); + } + + protected override Expression VisitGoto(GotoExpression node) + { + throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions."); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLabel(LabelExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLoop(LoopExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions."); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method == CreateDelegateMethod) + { + var visited = Visit(node.Arguments[1]); + Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, _enableDataValidation)); + return node; + } + else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) + { + if (node.Method.IsStatic) + { + Visit(node.Arguments[0]); + } + else + { + Visit(node.Object); + } + Nodes.Add(new StreamNode()); + return node; + } + + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName) + { + return Visit(Expression.MakeIndex(node.Object, null, node.Arguments)); + } + + throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'."); + } + + private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) + { + var type = method.DeclaringType; + return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTry(TryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 8cbcaa8233..ee91f964ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName) { - if (!propertyName.Contains(".")) - { - return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); - } - else - { - var split = propertyName.Split('.'); - - if (split.Length == 2) - { - // HACK: We need a way to resolve types here using something like IXamlTypeResolver. - // We don't currently have that so we have to make our best guess. - var type = split[0]; - var name = split[1]; - var registry = AvaloniaPropertyRegistry.Instance; - var registered = registry.GetRegisteredAttached(o.GetType()) - .Concat(registry.GetRegistered(o.GetType())); - - foreach (var p in registered) - { - if (p.Name == name && IsOfType(p.OwnerType, type)) - { - return p; - } - } - } - } - - return null; + return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); } private static bool IsOfType(Type type, string typeName) @@ -145,15 +117,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 observer) + protected override void UnsubscribeCore() { - _subscription = Instance?.GetWeakObservable(_property).Subscribe(observer); + _subscription?.Dispose(); + _subscription = null; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs index bd429f04d6..03ab7712bd 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs @@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins /// The value. void IObserver.OnNext(object value) => InnerValueChanged(value); - /// - protected override void Dispose(bool disposing) => _inner.Dispose(); - /// /// Begins listening to the inner . /// - protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged); + + /// + protected override void UnsubscribeCore() => _inner.Dispose(); /// /// Called when the inner 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); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 35f9f7e59a..4507b32e0c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/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; diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index d7dda57a72..33ea5bba08 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/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 /// /// - public interface IPropertyAccessor : IObservable, IDisposable + public interface IPropertyAccessor : IDisposable { /// /// 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. /// bool SetValue(object value, BindingPriority priority); + + /// + /// Subscribes to the value of the member. + /// + /// A method that receives the values. + void Subscribe(Action listener); + + /// + /// Unsubscribes to the value of the member. + /// + void Unsubscribe(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 436046f3fa..4d6fc01229 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/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 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) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..dab32b639a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/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 observer) - { - SendCurrentValue(); - SubscribeToChanges(); - } - private void SendCurrentValue() { try { var value = Value; - Observer.OnNext(value); + PublishValue(value); } catch { } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index b2b3a107fa..cf0abc6f35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/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 observer) + protected override void SubscribeCore() { try { - Observer.OnNext(Value); + PublishValue(Value); } catch { } } + + protected override void UnsubscribeCore() + { + } } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index 9cc78369a7..e840b2c5c9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/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 { /// /// Defines a default base implementation for a . /// - /// - /// is an observable that will only be subscribed to one time. - /// In addition, the subscription can be disposed by calling on the - /// property accessor itself - this prevents needing to hold two references for a subscription. - /// public abstract class PropertyAccessorBase : IPropertyAccessor { + private Action _listener; + /// public abstract Type PropertyType { get; } /// public abstract object Value { get; } - /// - /// Stops the subscription. - /// - public void Dispose() => Dispose(true); + /// + public void Dispose() + { + if (_listener != null) + { + Unsubscribe(); + } + } /// public abstract bool SetValue(object value, BindingPriority priority); - /// - /// The currently subscribed observer. - /// - protected IObserver Observer { get; private set; } - /// - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) { - Contract.Requires(observer != null); + Contract.Requires(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; + } + + /// + /// Publishes a value to the listener. + /// + /// The value. + protected void PublishValue(object value) => _listener?.Invoke(value); + /// - /// Stops listening to the property. + /// When overridden in a derived class, begins listening to the member. /// - /// - /// True if the method was called, false if the object is being - /// finalized. - /// - protected virtual void Dispose(bool disposing) => Observer = null; + protected abstract void SubscribeCore(); /// - /// When overridden in a derived class, begins listening to the property. + /// When overridden in a derived class, stops listening to the member. /// - protected abstract void SubscribeCore(IObserver observer); + protected abstract void UnsubscribeCore(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs index 647adc36cb..eb2400807a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs +++ b/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 observer) + public void Subscribe(Action listener) + { + listener(_error); + } + + public void Unsubscribe() { - observer.OnNext(_error); - return Disposable.Empty; } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 9d657b3144..a916142675 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -3,14 +3,12 @@ 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 { - internal class PropertyAccessorNode : SettableNode + public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; @@ -39,7 +37,7 @@ namespace Avalonia.Data.Core return false; } - protected override IObservable 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; } } } diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 092cdbe48f..e7c6ab766f 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Avalonia.Data.Core { - internal abstract class SettableNode : ExpressionNode + public abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs new file mode 100644 index 0000000000..fa8b56765c --- /dev/null +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia +{ + public static class StreamBindingExtensions + { + internal static string StreamBindingName = "StreamBinding"; + + public static T StreamBinding(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + + public static object StreamBinding(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + + public static T StreamBinding(this IObservable @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 187c79af49..6fc178e7f8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/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 + public class StreamNode : ExpressionNode { + private IDisposable _subscription; + public override string Description => "^"; - protected override IObservable 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; + } } } diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index f44f9043f0..7afbcabd2a 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics { var set = o.GetSetValues(); - PriorityValue value; - - if (set.TryGetValue(property, out value)) + if (set.TryGetValue(property, out var obj)) { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - (BindingPriority)value.ValuePriority, - value.GetDiagnostic()); + if (obj is PriorityValue value) + { + return new AvaloniaPropertyValue( + property, + o.GetValue(property), + (BindingPriority)value.ValuePriority, + value.GetDiagnostic()); + } + else + { + return new AvaloniaPropertyValue( + property, + obj, + BindingPriority.LocalValue, + "Local value"); + } } else { diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index aeec720920..8cbf212381 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data; +using Avalonia.Utilities; namespace Avalonia { @@ -13,22 +14,25 @@ namespace Avalonia /// /// Called when a 's value changes. /// - /// The source of the change. + /// The the property that has changed. + /// The priority of the value. /// The old value. /// The new value. - void Changed(PriorityValue sender, object oldValue, object newValue); + void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue); /// /// Called when a is received by a /// . /// - /// The source of the change. + /// The the property that has changed. /// The notification. - void BindingNotificationReceived(PriorityValue sender, BindingNotification notification); + void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); /// /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); + + DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 9b5318083a..03094e2236 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -21,7 +21,7 @@ namespace Avalonia /// priority binding that doesn't return . Where there /// are multiple bindings registered with the same priority, the most recently added binding /// has a higher priority. Each time the value changes, the - /// method on the + /// method on the /// owner object is fired with the old and new values. /// internal class PriorityValue @@ -30,7 +30,6 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; - private static readonly DeferredSetter delayedSetter = new DeferredSetter(); private (object value, int priority) _value; /// @@ -243,12 +242,18 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - delayedSetter.SetAndNotify(this, + Owner.Setter.SetAndNotify(Property, ref _value, UpdateCore, (value, priority)); } + private bool UpdateCore( + object update, + ref (object value, int priority) backing, + Action notify) + => UpdateCore(((object, int))update, ref backing, notify); + private bool UpdateCore( (object value, int priority) update, ref (object value, int priority) backing, @@ -281,12 +286,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 diff --git a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaObservable.cs deleted file mode 100644 index b59d8e5226..0000000000 --- a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs +++ /dev/null @@ -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 -{ - /// - /// An with an additional description. - /// - /// The type of the elements in the sequence. - public class AvaloniaObservable : ObservableBase, IDescription - { - private readonly Func, IDisposable> _subscribe; - - /// - /// Initializes a new instance of the class. - /// - /// The subscribe function. - /// The description of the observable. - public AvaloniaObservable(Func, IDisposable> subscribe, string description) - { - Contract.Requires(subscribe != null); - - _subscribe = subscribe; - Description = description; - } - - /// - /// Gets the description of the observable. - /// - public string Description { get; } - - /// - protected override IDisposable SubscribeCore(IObserver observer) - { - return _subscribe(observer) ?? Disposable.Empty; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs new file mode 100644 index 0000000000..5ef0d25133 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyChangedObservable : + LightweightObservableBase, + IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyChangedObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(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); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs new file mode 100644 index 0000000000..4385ab13ef --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -0,0 +1,52 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + + public AvaloniaPropertyObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(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 observer, bool first) + { + observer.OnNext(_value); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + _value = (T)e.NewValue; + PublishNext(_value); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs new file mode 100644 index 0000000000..a2786d63da --- /dev/null +++ b/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 +{ + /// + /// Lightweight base class for observable implementations. + /// + /// The observable type. + /// + /// 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. + /// + public abstract class LightweightObservableBase : IObservable + { + private Exception _error; + private List> _observers = new List>(); + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(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 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 _parent; + + IObserver _observer; + + public RemoveObserver(LightweightObservableBase parent, IObserver 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[] 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[] 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[] 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 observer, bool first) + { + } + } +} diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs new file mode 100644 index 0000000000..cd8ce2cd80 --- /dev/null +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + public abstract class SingleSubscriberObservableBase : IObservable, IDisposable + { + private Exception _error; + private IObserver _observer; + private bool _completed; + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(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(); + } +} diff --git a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs deleted file mode 100644 index aa5f553865..0000000000 --- a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs +++ /dev/null @@ -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, - IWeakSubscriber, IDescription - { - private WeakReference _sourceReference; - private readonly AvaloniaProperty _property; - private readonly Subject _changed = new Subject(); - - private int _count; - - public WeakPropertyChangedObservable( - WeakReference 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 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); - } - } - } - } -} diff --git a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 89% rename from src/Avalonia.Base/Data/Core/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index 14187c769a..0910d5b969 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -3,14 +3,14 @@ using System; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Utilities { - internal class Reader + public class CharacterReader { private readonly string _s; private int _i; - public Reader(string s) + public CharacterReader(string s) { _s = s; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index fdfa160134..ae6f599005 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -8,11 +8,10 @@ namespace Avalonia.Utilities { /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. /// - /// The type of the object that represents the property. /// The type of value with which to track the delayed assignment. - class DeferredSetter - where TProperty: class + class DeferredSetter { private struct NotifyDisposable : IDisposable { @@ -37,29 +36,44 @@ namespace Avalonia.Utilities { public bool Notifying { get; set; } - private Queue pendingValues; + private SingleOrQueue pendingValues; - public Queue PendingValues + public SingleOrQueue PendingValues { get { - return pendingValues ?? (pendingValues = new Queue()); + return pendingValues ?? (pendingValues = new SingleOrQueue()); } } } - private readonly ConditionalWeakTable setRecords = new ConditionalWeakTable(); + private Dictionary _setRecords; + private Dictionary SetRecords + => _setRecords ?? (_setRecords = new Dictionary()); + + private SettingStatus GetOrCreateStatus(AvaloniaProperty property) + { + if (!SetRecords.TryGetValue(property, out var status)) + { + status = new SettingStatus(); + SetRecords.Add(property, status); + } + + return status; + } /// /// Mark the property as currently notifying. /// /// The property to mark as notifying. /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(TProperty property) + private NotifyDisposable MarkNotifying(AvaloniaProperty property) { Contract.Requires(!IsNotifying(property)); - - return new NotifyDisposable(setRecords.GetOrCreateValue(property)); + + SettingStatus status = GetOrCreateStatus(property); + + return new NotifyDisposable(status); } /// @@ -67,19 +81,19 @@ namespace Avalonia.Utilities /// /// The property. /// If the property is currently notifying listeners. - private bool IsNotifying(TProperty property) - => setRecords.TryGetValue(property, out var value) && value.Notifying; + private bool IsNotifying(AvaloniaProperty property) + => SetRecords.TryGetValue(property, out var value) && value.Notifying; /// /// Add a pending assignment for the property. /// /// The property. /// The value to assign. - private void AddPendingSet(TProperty property, TSetRecord value) + private void AddPendingSet(AvaloniaProperty property, TSetRecord value) { Contract.Requires(IsNotifying(property)); - setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); + GetOrCreateStatus(property).PendingValues.Enqueue(value); } /// @@ -87,9 +101,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// If the property has any pending assignments. - private bool HasPendingSet(TProperty property) + private bool HasPendingSet(AvaloniaProperty property) { - return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; + return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; } /// @@ -97,9 +111,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(TProperty property) + private TSetRecord GetFirstPendingSet(AvaloniaProperty property) { - return setRecords.GetOrCreateValue(property).PendingValues.Dequeue(); + return GetOrCreateStatus(property).PendingValues.Dequeue(); } public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); @@ -115,7 +129,7 @@ namespace Avalonia.Utilities /// /// The value to try to set. public bool SetAndNotify( - TProperty property, + AvaloniaProperty property, ref TValue backing, SetterDelegate setterCallback, TSetRecord value) @@ -144,6 +158,7 @@ namespace Avalonia.Utilities } }); } + return updated; } else if(!object.Equals(value, backing)) diff --git a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 91% rename from src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index b0a9ff4df2..14b8affbdd 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Text; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Utilities { - internal static class IdentifierParser + public static class IdentifierParser { - public static string Parse(Reader r) + public static string Parse(CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Avalonia.Base/Utilities/SingleOrQueue.cs b/src/Avalonia.Base/Utilities/SingleOrQueue.cs new file mode 100644 index 0000000000..4a66b72a56 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SingleOrQueue.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Utilities +{ + /// + /// FIFO Queue optimized for holding zero or one items. + /// + /// The type of items held in the queue. + public class SingleOrQueue + { + private T _head; + private Queue _tail; + + private Queue Tail => _tail ?? (_tail = new Queue()); + + private bool HasTail => _tail != null; + + public bool Empty { get; private set; } = true; + + public void Enqueue(T value) + { + if (Empty) + { + _head = value; + } + else + { + Tail.Enqueue(value); + } + + Empty = false; + } + + public T Dequeue() + { + if (Empty) + { + throw new InvalidOperationException("Cannot dequeue from an empty queue!"); + } + + var result = _head; + + if (HasTail && Tail.Count != 0) + { + _head = Tail.Dequeue(); + } + else + { + _head = default; + Empty = true; + } + + return result; + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs new file mode 100644 index 0000000000..ab80e74923 --- /dev/null +++ b/src/Avalonia.Base/ValueStore.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia +{ + internal class ValueStore : IPriorityValueOwner + { + private readonly AvaloniaObject _owner; + private readonly Dictionary _values = + new Dictionary(); + + public ValueStore(AvaloniaObject owner) + { + _owner = owner; + } + + public IDisposable AddBinding( + AvaloniaProperty property, + IObservable 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) + { + _owner.BindingNotificationReceived(property, notification); + } + + public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + { + _owner.PriorityValueChanged(property, priority, oldValue, newValue); + } + + public IDictionary GetSetValues() => _values; + + 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 is PriorityValue priority && priority.IsAnimating; + } + + 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 validate2 = null; + + if (validate != null) + { + validate2 = v => validate(_owner, v); + } + + return new PriorityValue( + this, + property, + property.PropertyType, + validate2); + } + + 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; + } + + private DeferredSetter _defferedSetter; + + public DeferredSetter Setter + { + get + { + return _defferedSetter ?? + (_defferedSetter = new DeferredSetter()); + } + } + } +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 51b690ece9..83763c0836 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ namespace Avalonia.Controls }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -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()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + 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().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - 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() + 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(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557eb..499b65c5b7 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,19 +209,74 @@ namespace Avalonia /// The closable to track 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); + } + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + 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); + } } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track 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); + } } /// @@ -191,7 +284,13 @@ namespace Avalonia /// public void Exit() { + IsExiting = true; + + Windows.Clear(); + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// @@ -233,7 +332,6 @@ namespace Avalonia .Bind().ToConstant(InputManager) .Bind().ToTransient() .Bind().ToConstant(_styler) - .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance) .Bind().ToConstant(DragDropDevice.Instance) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 78dc994df7..13f00bdc87 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,11 +7,20 @@ namespace Avalonia.Controls using System; using System.Reactive.Linq; using System.Linq; + using System.ComponentModel; + public class ContextMenu : SelectingItemsControl { private bool _isOpen; private Popup _popup; + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); + + /// /// Initializes static members of the class. /// @@ -22,6 +31,26 @@ namespace Avalonia.Controls MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } + /// + /// Gets a value indicating whether the popup is open + /// + public bool IsOpen => _isOpen; + + /// + /// Occurs when the value of the + /// + /// property is changing from false to true. + /// + public event CancelEventHandler ContextMenuOpening; + + /// + /// Occurs when the value of the + /// + /// property is changing from true to false. + /// + public event CancelEventHandler ContextMenuClosing; + + /// /// Called when the property changes on a control. /// @@ -59,12 +88,12 @@ namespace Avalonia.Controls { if (_popup != null && _popup.IsVisible) { - _popup.Close(); + _popup.IsOpen = false; } SelectedIndex = -1; - _isOpen = false; + SetAndRaise(IsOpenProperty, ref _isOpen, false); } /// @@ -89,11 +118,11 @@ namespace Avalonia.Controls } ((ISetLogicalParent)_popup).SetParent(control); - _popup.Child = control.ContextMenu; + _popup.Child = this; - _popup.Open(); + _popup.IsOpen = true; - control.ContextMenu._isOpen = true; + SetAndRaise(IsOpenProperty, ref _isOpen, true); } } @@ -118,21 +147,37 @@ namespace Avalonia.Controls var control = (Control)sender; var contextMenu = control.ContextMenu; - if (e.MouseButton == MouseButton.Right) + if (control.ContextMenu._isOpen) { - if (control.ContextMenu._isOpen) - { - control.ContextMenu.Hide(); - } + if (contextMenu.CancelClosing()) + return; - contextMenu.Show(control); + control.ContextMenu.Hide(); e.Handled = true; } - else if (contextMenu._isOpen) + + if (e.MouseButton == MouseButton.Right) { - control.ContextMenu.Hide(); + if (contextMenu.CancelOpening()) + return; + + contextMenu.Show(control); e.Handled = true; } } + + private bool CancelClosing() + { + var eventArgs = new CancelEventArgs(); + ContextMenuClosing?.Invoke(this, eventArgs); + return eventArgs.Cancel; + } + + private bool CancelOpening() + { + var eventArgs = new CancelEventArgs(); + ContextMenuOpening?.Invoke(this, eventArgs); + return eventArgs.Cancel; + } } } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 179dccaf76..c177d43917 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Embedding { EnsureInitialized(); ApplyTemplate(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 4db16c71a5..5becdc0f61 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { EnsureInitialized(); ApplyTemplate(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 0000000000..b73fe4a963 --- /dev/null +++ b/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 +{ + /// + /// Enum for ExitMode + /// + public enum ExitMode + { + /// + /// Indicates an implicit call to Application.Exit when the last window closes. + /// + OnLastWindowClose, + + /// + /// Indicates an implicit call to Application.Exit when the main window closes. + /// + OnMainWindowClose, + + /// + /// Indicates that the application only exits on an explicit call to Application.Exit. + /// + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index e7f75336f5..5323939b50 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -66,9 +66,7 @@ namespace Avalonia.Controls protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { - IVisual visualContent = Presenter; - - if (Content != null && ContentTransition != null && visualContent != null) + if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; @@ -87,4 +85,4 @@ namespace Avalonia.Controls private ExpandDirection _expandDirection; private bool _isExpanded; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 54fcefeb3f..5f194bdd71 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -194,6 +194,16 @@ namespace Avalonia.Controls /// private GridLayout.MeasureResult _rowMeasureCache; + /// + /// Gets the row layout as of the last measure. + /// + private GridLayout _rowLayoutCache; + + /// + /// Gets the column layout as of the last measure. + /// + private GridLayout _columnLayoutCache; + /// /// Measures the grid. /// @@ -253,6 +263,9 @@ namespace Avalonia.Controls // Cache the measure result and return the desired size. _columnMeasureCache = columnResult; _rowMeasureCache = rowResult; + _rowLayoutCache = rowLayout; + _columnLayoutCache = columnLayout; + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); // Measure each child only once. @@ -299,13 +312,11 @@ namespace Avalonia.Controls // arrow back to any statements and re-run them without any side-effect. var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - + var columnLayout = _columnLayoutCache; + var rowLayout = _rowLayoutCache; // Calculate for arrange result. var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); - // Arrange the children. foreach (var child in Children.OfType()) { @@ -315,7 +326,6 @@ namespace Avalonia.Controls var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index f6f11aa9ad..f146e3571c 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,12 +1,11 @@ // 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.Media; using Avalonia.Media.Imaging; namespace Avalonia.Controls -{ +{ /// /// Displays a image. /// @@ -68,7 +67,9 @@ namespace Avalonia.Controls Rect sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)); - context.DrawImage(source, 1, sourceRect, destRect); + var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); + + context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); } } @@ -100,4 +101,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 5119096965..3cb997f615 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -155,6 +155,7 @@ namespace Avalonia.Controls void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { Presenter = presenter; + ItemContainerGenerator.Clear(); } /// diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index daaf3fd5cf..95193c0432 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -49,11 +49,9 @@ namespace Avalonia.Controls.Mixins Contract.Requires(content != null); Contract.Requires(logicalChildrenSelector != null); - EventHandler 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); } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index fee326dacc..e293cff211 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -161,6 +161,11 @@ namespace Avalonia.Controls.Presenters /// An . public static ItemVirtualizer Create(ItemsPresenter owner) { + if (owner.Panel == null) + { + return null; + } + var virtualizingPanel = owner.Panel as IVirtualizingPanel; var scrollable = (ILogicalScrollable)owner; ItemVirtualizer result = null; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index c1489e7138..b98f26b87f 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -518,7 +518,7 @@ namespace Avalonia.Controls.Presenters } var container = generator.ContainerFromIndex(index); - var layoutManager = LayoutManager.Instance; + var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager; // We need to do a layout here because it's possible that the container we moved to // is only partially visible due to differing item sizes. If the container is only diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 590bfa25ac..f8d62a1cbf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters nameof(VirtualizationMode), defaultValue: ItemVirtualizationMode.None); - private ItemVirtualizer _virtualizer; private bool _canHorizontallyScroll; private bool _canVerticallyScroll; @@ -76,21 +75,27 @@ namespace Avalonia.Controls.Presenters /// bool ILogicalScrollable.IsLogicalScrollEnabled { - get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } + get { return Virtualizer?.IsLogicalScrollEnabled ?? false; } } /// - Size IScrollable.Extent => _virtualizer.Extent; + Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; /// Vector IScrollable.Offset { - get { return _virtualizer.Offset; } - set { _virtualizer.Offset = CoerceOffset(value); } + get { return Virtualizer?.Offset ?? new Vector(); } + set + { + if (Virtualizer != null) + { + Virtualizer.Offset = CoerceOffset(value); + } + } } /// - Size IScrollable.Viewport => _virtualizer.Viewport; + Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size; /// Action ILogicalScrollable.InvalidateScroll { get; set; } @@ -101,6 +106,8 @@ namespace Avalonia.Controls.Presenters /// Size ILogicalScrollable.PageScrollSize => new Size(0, 1); + internal ItemVirtualizer Virtualizer { get; private set; } + /// bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { @@ -110,29 +117,30 @@ namespace Avalonia.Controls.Presenters /// IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) { - return _virtualizer?.GetControlInDirection(direction, from); + return Virtualizer?.GetControlInDirection(direction, from); } public override void ScrollIntoView(object item) { - _virtualizer?.ScrollIntoView(item); + Virtualizer?.ScrollIntoView(item); } /// protected override Size MeasureOverride(Size availableSize) { - return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; } protected override Size ArrangeOverride(Size finalSize) { - return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; + return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; } /// protected override void PanelCreated(IPanel panel) { - _virtualizer = ItemVirtualizer.Create(this); + Virtualizer?.Dispose(); + Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) @@ -149,7 +157,7 @@ namespace Avalonia.Controls.Presenters protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) { - _virtualizer?.ItemsChanged(Items, e); + Virtualizer?.ItemsChanged(Items, e); } private Vector CoerceOffset(Vector value) @@ -162,8 +170,8 @@ namespace Avalonia.Controls.Presenters private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) { - _virtualizer?.Dispose(); - _virtualizer = ItemVirtualizer.Create(this); + Virtualizer?.Dispose(); + Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 5a56e52029..e9dc75a236 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Styling; @@ -40,6 +41,7 @@ namespace Avalonia.Controls.Presenters ItemsControl.MemberSelectorProperty.AddOwner(); private IEnumerable _items; + private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; @@ -63,24 +65,12 @@ namespace Avalonia.Controls.Presenters set { - if (_createdPanel) - { - INotifyCollectionChanged incc = _items as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged -= ItemsCollectionChanged; - } - } + _itemsSubscription?.Dispose(); + _itemsSubscription = null; - if (_createdPanel && value != null) + if (_createdPanel && value is INotifyCollectionChanged incc) { - INotifyCollectionChanged incc = value as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged += ItemsCollectionChanged; - } + _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } SetAndRaise(ItemsProperty, ref _items, value); @@ -233,11 +223,9 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - INotifyCollectionChanged incc = Items as INotifyCollectionChanged; - - if (incc != null) + if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) { - incc.CollectionChanged += ItemsCollectionChanged; + _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } PanelCreated(Panel); @@ -263,4 +251,4 @@ namespace Avalonia.Controls.Presenters (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bda660be51..636c836da5 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters _caretTimer.Start(); InvalidateVisual(); } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } if (IsMeasureValid) { diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 51c22c88e7..676cdc456a 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/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() diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 457a7bd4b4..0ae4be5550 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -83,21 +83,22 @@ namespace Avalonia.Controls.Primitives /// public void SnapInsideScreenEdges() { - var window = this.GetSelfAndLogicalAncestors().OfType().First(); - - var screen = window.Screens.ScreenFromPoint(Position); + var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position); - var screenX = Position.X + Bounds.Width - screen.Bounds.X; - var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) + if (screen != null) { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } + var screenX = Position.X + Bounds.Width - screen.Bounds.X; + var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + if (screenX > screen.Bounds.Width) + { + Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); + } + + if (screenY > screen.Bounds.Height) + { + Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2e668fda95..c8425a0f80 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives { if (!AlwaysSelected) { - SelectedIndex = -1; + selectedIndex = SelectedIndex = -1; } else { @@ -368,10 +368,15 @@ namespace Avalonia.Controls.Primitives } } + var items = Items?.Cast(); + if (selectedIndex >= items.Count()) + { + selectedIndex = SelectedIndex = items.Count() - 1; + } break; case NotifyCollectionChangedAction.Reset: - SelectedIndex = IndexOf(e.NewItems, SelectedItem); + SelectedIndex = IndexOf(Items, SelectedItem); break; } } @@ -408,12 +413,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; + } } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8514104c91..296134ca48 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/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(); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index dc9b70ab8c..1d2e2f2100 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives nameof(IsChecked), o => o.IsChecked, (o, v) => o.IsChecked = v, - unsetValue: false, + unsetValue: null, defaultBindingMode: BindingMode.TwoWay); public static readonly StyledProperty IsThreeStateProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 5e5a460368..b7db352c74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -21,18 +21,27 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + private static readonly DirectProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.RegisterDirect( + nameof(IndeterminateStartingOffset), + p => p.IndeterminateStartingOffset, + (p, o) => p.IndeterminateStartingOffset = o); + + private static readonly DirectProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.RegisterDirect( + nameof(IndeterminateEndingOffset), + p => p.IndeterminateEndingOffset, + (p, o) => p.IndeterminateEndingOffset = o); + private Border _indicator; - private IndeterminateAnimation _indeterminateAnimation; static ProgressBar() { PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - - IsIndeterminateProperty.Changed.AddClassHandler( - (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); } public bool IsIndeterminate @@ -46,6 +55,19 @@ namespace Avalonia.Controls get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } + private double _indeterminateStartingOffset; + private double IndeterminateStartingOffset + { + get => _indeterminateStartingOffset; + set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + } + + private double _indeterminateEndingOffset; + private double IndeterminateEndingOffset + { + get => _indeterminateEndingOffset; + set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + } /// protected override Size ArrangeOverride(Size finalSize) @@ -60,7 +82,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); - UpdateIsIndeterminate(IsIndeterminate); } private void UpdateIndicator(Size bounds) @@ -70,9 +91,20 @@ namespace Avalonia.Controls if (IsIndeterminate) { if (Orientation == Orientation.Horizontal) - _indicator.Width = bounds.Width / 5.0; + { + var width = bounds.Width / 5.0; + IndeterminateStartingOffset = -width; + _indicator.Width = width; + IndeterminateEndingOffset = bounds.Width; + + } else - _indicator.Height = bounds.Height / 5.0; + { + var height = bounds.Height / 5.0; + IndeterminateStartingOffset = -bounds.Height; + _indicator.Height = height; + IndeterminateEndingOffset = height; + } } else { @@ -86,53 +118,9 @@ namespace Avalonia.Controls } } - private void UpdateIsIndeterminate(bool isIndeterminate) - { - if (isIndeterminate) - { - if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed) - _indeterminateAnimation = IndeterminateAnimation.StartAnimation(this); - } - else - _indeterminateAnimation?.Dispose(); - } - private void ValueChanged(AvaloniaPropertyChangedEventArgs e) { UpdateIndicator(Bounds.Size); } - - // TODO: Implement Indeterminate Progress animation - // in xaml (most ideal) or if it's not possible - // then on this class. - private class IndeterminateAnimation : IDisposable - { - private WeakReference _progressBar; - - private bool _disposed; - - public bool Disposed => _disposed; - - private IndeterminateAnimation(ProgressBar progressBar) - { - _progressBar = new WeakReference(progressBar); - - } - - public static IndeterminateAnimation StartAnimation(ProgressBar progressBar) - { - return new IndeterminateAnimation(progressBar); - } - - private Rect GetAnimationRect(TimeSpan time) - { - return Rect.Empty; - } - - public void Dispose() - { - _disposed = true; - } - } } } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 2ef0af2852..31113812d1 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + ScrollBar.OrientationProperty.AddOwner(); /// /// Defines the property. @@ -41,8 +41,7 @@ namespace Avalonia.Controls /// static Slider() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index a6fe35d668..b0ccd8a3d1 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Input; namespace Avalonia.Controls @@ -12,10 +13,10 @@ namespace Avalonia.Controls public class StackPanel : Panel, INavigableContainer { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty GapProperty = - AvaloniaProperty.Register(nameof(Gap)); + public static readonly StyledProperty SpacingProperty = + AvaloniaProperty.Register(nameof(Spacing)); /// /// Defines the property. @@ -28,17 +29,17 @@ namespace Avalonia.Controls /// static StackPanel() { - AffectsMeasure(GapProperty); + AffectsMeasure(SpacingProperty); AffectsMeasure(OrientationProperty); } /// - /// Gets or sets the size of the gap to place between child controls. + /// Gets or sets the size of the spacing to place between child controls. /// - public double Gap + public double Spacing { - get { return GetValue(GapProperty); } - set { SetValue(GapProperty, value); } + get { return GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } } /// @@ -151,7 +152,8 @@ namespace Avalonia.Controls double measuredWidth = 0; double measuredHeight = 0; - double gap = Gap; + double spacing = Spacing; + bool hasVisibleChild = Children.Any(c => c.IsVisible); foreach (Control child in Children) { @@ -160,23 +162,23 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { - measuredHeight += size.Height + gap; + measuredHeight += size.Height + (child.IsVisible ? spacing : 0); measuredWidth = Math.Max(measuredWidth, size.Width); } else { - measuredWidth += size.Width + gap; + measuredWidth += size.Width + (child.IsVisible ? spacing : 0); measuredHeight = Math.Max(measuredHeight, size.Height); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= gap; + measuredHeight -= (hasVisibleChild ? spacing : 0); } else { - measuredWidth -= gap; + measuredWidth -= (hasVisibleChild ? spacing : 0); } return new Size(measuredWidth, measuredHeight); @@ -192,7 +194,8 @@ namespace Avalonia.Controls var orientation = Orientation; double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; - double gap = Gap; + double spacing = Spacing; + bool hasVisibleChild = Children.Any(c => c.IsVisible); if (Orientation == Orientation.Vertical) { @@ -214,25 +217,25 @@ namespace Avalonia.Controls Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); - arrangedHeight += childHeight + gap; + arrangedHeight += childHeight + (child.IsVisible ? spacing : 0); } else { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); ArrangeChild(child, childFinal, finalSize, orientation); - arrangedWidth += childWidth + gap; + arrangedWidth += childWidth + (child.IsVisible ? spacing : 0); arrangedHeight = Math.Max(arrangedHeight, childHeight); } } if (orientation == Orientation.Vertical) { - arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); + arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height); } else { - arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); + arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width); } return new Size(arrangedWidth, arrangedHeight); @@ -247,4 +250,4 @@ namespace Avalonia.Controls child.Arrange(rect); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 2ea9319194..388f984b78 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); - _undoRedoHelper = new UndoRedoHelper(this); + _undoRedoHelper = new UndoRedoHelper(this); } public bool AcceptsReturn @@ -262,7 +262,7 @@ namespace Avalonia.Controls if (IsFocused) { - _presenter.ShowCaret(); + DecideCaretVisibility(); } } @@ -282,12 +282,20 @@ namespace Avalonia.Controls } else { - _presenter?.ShowCaret(); + DecideCaretVisibility(); } e.Handled = true; } + private void DecideCaretVisibility() + { + if (!IsReadOnly) + _presenter?.ShowCaret(); + else + _presenter?.HideCaret(); + } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -557,7 +565,7 @@ namespace Avalonia.Controls var index = CaretIndex = _presenter.GetCaretIndex(point); var text = Text; - if (text != null) + if (text != null && e.MouseButton == MouseButton.Left) { switch (e.ClickCount) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1af347ab4e..9fdc097c3f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -54,6 +54,7 @@ namespace Avalonia.Controls private readonly IApplicationLifecycle _applicationLifecycle; private readonly IPlatformRenderInterface _renderInterface; private Size _clientSize; + private ILayoutManager _layoutManager; /// /// Initializes static members of the class. @@ -147,6 +148,16 @@ namespace Avalonia.Controls protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } + public ILayoutManager LayoutManager + { + get + { + if (_layoutManager == null) + _layoutManager = CreateLayoutManager(); + return _layoutManager; + } + } + /// /// Gets the platform-specific window implementation. /// @@ -235,6 +246,11 @@ namespace Avalonia.Controls { return PlatformImpl?.PointToScreen(p) ?? default(Point); } + + /// + /// Creates the layout manager for this . + /// + protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(); /// /// Handles a paint notification from . @@ -267,7 +283,7 @@ namespace Avalonia.Controls ClientSize = clientSize; Width = clientSize.Width; Height = clientSize.Height; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index dee537029c..5f7b63c57a 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -200,7 +200,7 @@ namespace Avalonia.Controls private void UpdateAdd(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; child.Measure(_availableSpace); ++_averageCount; @@ -208,13 +208,13 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace += height + gap; + _takenSpace += height + spacing; AddToAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace += width + gap; + _takenSpace += width + spacing; AddToAverageItemSize(width); } } @@ -222,18 +222,18 @@ namespace Avalonia.Controls private void UpdateRemove(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace -= height + gap; + _takenSpace -= height + spacing; RemoveFromAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace -= width + gap; + _takenSpace -= width + spacing; RemoveFromAverageItemSize(width); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..7e1d8f18f0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((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); } - + /// event EventHandler INameScope.Registered { @@ -199,7 +191,7 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -259,6 +251,26 @@ namespace Avalonia.Controls /// public event EventHandler 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); + } + /// /// Closes the window. /// @@ -290,18 +302,22 @@ namespace Avalonia.Controls internal void Close(bool ignoreCancel) { - var cancelClosing = false; + bool close = true; + try { - cancelClosing = HandleClosing(); + if (!ignoreCancel && HandleClosing()) + { + close = false; + return; + } } finally { - if (ignoreCancel || !cancelClosing) + if (close) { - s_windows.Remove(this); PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -313,6 +329,7 @@ namespace Avalonia.Controls { var args = new CancelEventArgs(); Closing?.Invoke(this, args); + return args.Cancel; } @@ -359,18 +376,18 @@ namespace Avalonia.Controls return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); - SetWindowStartupLocation(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) { PlatformImpl?.Show(); Renderer?.Start(); } + SetWindowStartupLocation(); } /// @@ -400,16 +417,16 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); 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 +530,8 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 5d66bee2f8..c0b664ebc3 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -179,10 +179,9 @@ namespace Avalonia.Controls if (!_hasExecutedInitialLayoutPass) { - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); _hasExecutedInitialLayoutPass = true; } - PlatformImpl?.Show(); Renderer?.Start(); } @@ -262,7 +261,7 @@ namespace Avalonia.Controls Height = clientSize.Height; } ClientSize = clientSize; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 0000000000..df79c3e3c8 --- /dev/null +++ b/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 + { + private readonly Application _application; + private readonly List _windows = new List(); + + public WindowCollection(Application application) + { + _application = application; + } + + /// + /// + /// Gets the number of elements in the collection. + /// + public int Count => _windows.Count; + + /// + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public Window this[int index] => _windows[index]; + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _windows.GetEnumerator(); + } + + /// + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + if (window == null) + { + return; + } + + _windows.Add(window); + } + + /// + /// Removes the specified window. + /// + /// The window. + internal void Remove(Window window) + { + if (window == null) + { + return; + } + + _windows.Remove(window); + + OnRemoveWindow(window); + } + + /// + /// Closes all windows and removes them from the underlying collection. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(true); + } + } + + 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; + } + } + } +} diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 9f54137e47..5ccb98b64d 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -1,7 +1,12 @@  netstandard2.0 - false + + 0.7.0 true @@ -37,11 +42,6 @@ - - - Properties\SharedAssemblyInfo.cs - - \ No newline at end of file diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index bb1b2e841e..844670e794 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -7,7 +7,7 @@ - + Hold Ctrl+Shift over a control to inspect. Focused: @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index 555a0b2354..ce8ad36c17 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels } }; + SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index a715ca6fc5..57398851ad 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -5,7 +5,7 @@ - + @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index f80462e958..0aed0a9717 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs index 25a6331b38..700b6a8600 100644 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ b/src/Avalonia.Layout/ILayoutRoot.cs @@ -22,5 +22,10 @@ namespace Avalonia.Layout /// The scaling factor to use in layout. /// double LayoutScaling { get; } + + /// + /// Associated instance of layout manager + /// + ILayoutManager LayoutManager { get; } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b6b786a077..9cf8521008 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -19,11 +19,6 @@ namespace Avalonia.Layout private bool _queued; private bool _running; - /// - /// Gets the layout manager. - /// - public static ILayoutManager Instance => AvaloniaLocator.Current.GetService(); - /// public void InvalidateMeasure(ILayoutable control) { @@ -170,7 +165,7 @@ namespace Avalonia.Layout { root.Measure(Size.Infinity); } - else + else if (control.PreviousMeasure.HasValue) { control.Measure(control.PreviousMeasure.Value); } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 523c720e2f..7112e216ff 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -389,7 +389,7 @@ namespace Avalonia.Layout if (((ILayoutable)this).IsAttachedToVisualTree) { - LayoutManager.Instance?.InvalidateMeasure(this); + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); InvalidateVisual(); } OnMeasureInvalidated(); @@ -406,12 +406,8 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - - if (((ILayoutable)this).IsAttachedToVisualTree) - { - LayoutManager.Instance?.InvalidateArrange(this); - InvalidateVisual(); - } + (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); + InvalidateVisual(); } } diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs index 1f25fa132d..1586c61185 100644 --- a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs +++ b/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 GetResourceObservable(this IResourceNode target, string key) { - return Observable.FromEventPattern( - x => target.ResourcesChanged += x, - x => target.ResourcesChanged -= x) - .StartWith((EventPattern)null) - .Select(x => target.FindResource(key)); + return new ResourceObservable(target, key); + } + + private class ResourceObservable : LightweightObservableBase + { + 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 observer, bool first) + { + observer.OnNext(_target.FindResource(_key)); + } + + private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + PublishNext(_target.FindResource(_key)); + } } } } diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 2858d11d9d..7186143bf9 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/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 /// The name of the control to find. public static IObservable Track(ILogical relativeTo, string name) { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => ((ILogical)x.Sender).FindNameScope()) - .StartWith(relativeTo.FindNameScope()); - - var detached = Observable.FromEventPattern( - 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 Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) + { + return new ControlTracker(relativeTo, ancestorLevel, ancestorType); + } + + private class ControlTracker : LightweightObservableBase + { + 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( - x => nameScope.Registered += x, - x => nameScope.Registered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(x => x.EventArgs.Element) - .OfType(); - var unregistered = Observable.FromEventPattern( - x => nameScope.Unregistered += x, - x => nameScope.Unregistered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(_ => (ILogical)null); - return registered - .StartWith(nameScope.Find(name)) - .Merge(unregistered); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - else + + _value = null; + } + + protected override void Subscribed(IObserver 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(null); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - }).Switch(); - } - public static IObservable 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 TrackAttachmentToTree(ILogical relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToLogicalTree); - - var detached = Observable.FromEventPattern( - 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(_name); + } + else + { + _value = null; + } + } + else + { + _value = _relativeTo.GetLogicalAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); + } + } } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 2cd324fff4..5b2774943a 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/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. /// /// - /// An has two inputs: an activator observable a + /// An has two inputs: an activator observable and a /// observable which produces the activated value. When the activator /// produces true, the will produce the current activated /// value. When the activator produces false it will produce /// . /// - internal class ActivatedObservable : ObservableBase, IDescription + internal class ActivatedObservable : ActivatedValue, IDescription { + private IDisposable _sourceSubscription; + /// /// Initializes a new instance of the class. /// @@ -29,49 +29,49 @@ namespace Avalonia.Styling IObservable activator, IObservable source, string description) + : base(activator, AvaloniaProperty.UnsetValue, description) { - Contract.Requires(activator != null); Contract.Requires(source != null); - Activator = activator; - Description = description; Source = source; } - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - /// /// Gets an observable which produces the . /// public IObservable Source { get; } - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) + protected override ActivatorListener CreateListener() => new ValueListener(this); + + protected override void Deinitialize() { - Contract.Requires(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 + { + 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.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); + void IObserver.OnNext(object value) => Parent.NotifyValue(value); } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs index 4c2e8dde85..a8446c4bfb 100644 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ b/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. /// /// - /// An has two inputs: an activator observable and either an - /// or a observable which produces the - /// activated value. When the activator produces true, the will - /// produce the current activated value. When the activator produces false it will produce - /// . + /// An extends to + /// be an . When the object is active then values + /// received via will be passed to the source subject. /// internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { - private bool? _active; private bool _completed; - private object _value; + private object _pushValue; /// /// Initializes a new instance of the class. @@ -35,7 +31,6 @@ namespace Avalonia.Styling string description) : base(activator, source, description) { - Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted); } /// @@ -46,53 +41,57 @@ namespace Avalonia.Styling get { return (ISubject)base.Source; } } - /// - /// Notifies all subscribed observers about the end of the sequence. - /// public void OnCompleted() { - if (_active.Value && !_completed) - { - Source.OnCompleted(); - } + Source.OnCompleted(); } - /// - /// Notifies all subscribed observers with the exception. - /// - /// The exception to send to all subscribed observers. - /// is null. public void OnError(Exception error) { - if (_active.Value && !_completed) - { - Source.OnError(error); - } + Source.OnError(error); } - /// - /// Notifies all subscribed observers with the value. - /// - /// The value to send to all subscribed observers. 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; } } diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index 3b9324c059..908d89b751 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/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 /// will produce the current value. When the activator /// produces false it will produce . /// - internal class ActivatedValue : ObservableBase, IDescription + internal class ActivatedValue : LightweightObservableBase, IDescription { - /// - /// The activator. - /// - private readonly IObservable _activator; + private static readonly object NotSent = new object(); + private IDisposable _activatorSubscription; + private object _value; + private object _last = NotSent; /// /// Initializes a new instance of the class. @@ -34,39 +33,101 @@ namespace Avalonia.Styling object value, string description) { - _activator = activator; + Contract.Requires(activator != null); + + Activator = activator; Value = value; Description = description; + Listener = CreateListener(); } /// - /// Gets the activated value. + /// Gets the activator observable. /// - public object Value - { - get; - } + public IObservable Activator { get; } /// /// Gets a description of the binding. /// - public string Description - { - get; - } + public string Description { get; } + + /// + /// Gets a value indicating whether the activator is active. + /// + public bool? IsActive { get; private set; } /// - /// Notifies the provider that an observer is to receive notifications. + /// Gets the value that will be produced when is true. /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver 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 observer, bool first) { - Contract.Requires(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 + { + public ActivatorListener(ActivatedValue parent) + { + Parent = parent; + } + + protected ActivatedValue Parent { get; } - return _activator - .Select(active => active ? Value : AvaloniaProperty.UnsetValue) - .Subscribe(observer); + void IObserver.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); + void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1a78e0f4d7..c75bae4db8 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -126,7 +126,7 @@ namespace Avalonia.Styling if (source != null) { - var cloned = Clone(source, style, activator); + var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); return BindingOperations.Apply(control, Property, cloned, null); } } @@ -134,13 +134,13 @@ namespace Avalonia.Styling return Disposable.Empty; } - private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable activator) + private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) { if (activator != null) { var description = style?.ToString(); - switch (sourceInstance.Mode) + switch (mode) { case BindingMode.OneTime: if (sourceInstance.Observable != null) @@ -158,18 +158,11 @@ namespace Avalonia.Styling var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); } - case BindingMode.OneWayToSource: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger); - } - case BindingMode.TwoWay: + default: { var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); + return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); } - default: - throw new NotSupportedException("Unsupported BindingMode."); } } diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs index 5bdea9f42e..63945037d8 100644 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ b/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(); } } } diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index fb32913e7e..94c0b75c6e 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/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)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 + { + readonly IList _match; + IAvaloniaReadOnlyList _classes; + bool _value; + + public ClassObserver(IAvaloniaReadOnlyList classes, IList 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 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; + } + } } } diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 82dbf6064b..6a9af487cb 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -16,7 +16,7 @@ DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" /> diff --git a/src/Avalonia.Themes.Default/DropDown.xaml b/src/Avalonia.Themes.Default/DropDown.xaml index 139b2b3687..c57d961f4b 100644 --- a/src/Avalonia.Themes.Default/DropDown.xaml +++ b/src/Avalonia.Themes.Default/DropDown.xaml @@ -32,7 +32,7 @@ diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index efb31175fa..53965db016 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -45,7 +45,7 @@ - - - - + @@ -35,4 +30,36 @@ - \ No newline at end of file + + + diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index d270384123..b24c863be9 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/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}"> @@ -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}"> diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 5355d5c7c3..c493fb32e3 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/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}"/> diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index 0944612417..58fd67b2f6 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -11,7 +11,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -72,8 +72,7 @@ + + "; var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); - var textBlock = window.FindControl("textBlock"); + var textBlock = (TextBlock)window.Content; - textBlock.DataContext = new { Foo = "world" }; + window.DataContext = 5.6; window.ApplyTemplate(); - Assert.Equal("Hello world", textBlock.Text); + Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); } } + + [Fact] + public void Binding_To_TextBlock_Text_With_StringConverter_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + textBlock.DataContext = new { Foo = "world" }; + window.ApplyTemplate(); + + Assert.Equal("Hello world", textBlock.Text); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 095aae7742..c6fe79bc0c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -297,7 +297,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("title", button.Content); } } + + [Fact] + public void Shorthand_Binding_With_Negation_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + +