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/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/packages.cake b/packages.cake index 79147b5f99..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 [] { /////////////////////////////////////////////////////////////////////////////// @@ -255,11 +272,7 @@ public class Packages "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", "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/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/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index baca1494be..761c0618da 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -29,12 +29,6 @@ namespace Avalonia /// 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. @@ -52,6 +46,7 @@ namespace Avalonia private EventHandler _propertyChanged; private DeferredSetter _directDeferredSetter; + private ValueStore _values; /// /// Delayed setter helper for direct properties. Used to fix #855. @@ -228,9 +223,20 @@ namespace Avalonia { return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); } + else if (_values != null) + { + var result = _values.GetValue(property); + + if (result == AvaloniaProperty.UnsetValue) + { + result = GetDefaultValue(property); + } + + return result; + } else { - return GetValueInternal(property); + return GetDefaultValue(property); } } @@ -257,7 +263,7 @@ namespace Avalonia Contract.Requires(property != null); VerifyAccess(); - return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + return _values?.IsAnimating(property) ?? false; } /// @@ -274,14 +280,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; } /// @@ -369,14 +368,6 @@ namespace Avalonia } else { - PriorityValue v; - - if (!_values.TryGetValue(property, out v)) - { - v = CreatePriorityValue(property); - _values.Add(property, v); - } - Logger.Verbose( LogArea.Property, this, @@ -385,7 +376,12 @@ namespace Avalonia description, priority); - return v.Add(source, (int)priority); + if (_values == null) + { + _values = new ValueStore(this); + } + + return _values.AddBinding(property, source, priority); } } @@ -416,20 +412,12 @@ 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) + void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) { - var property = sender.Property; - var priority = (BindingPriority)sender.ValuePriority; - oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : oldValue; @@ -439,7 +427,7 @@ namespace Avalonia if (!Equals(oldValue, newValue)) { - RaisePropertyChanged(property, oldValue, newValue, priority); + RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); Logger.Verbose( LogArea.Property, @@ -448,14 +436,14 @@ namespace Avalonia property, oldValue, newValue, - priority); + (BindingPriority)priority); } } /// - void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification) + void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { - UpdateDataValidation(sender.Property, notification); + UpdateDataValidation(property, notification); } /// @@ -468,10 +456,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. @@ -660,68 +645,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. /// @@ -802,21 +737,13 @@ namespace Avalonia originalValue?.GetType().FullName ?? "(null)")); } - PriorityValue v; - - if (!_values.TryGetValue(property, out v)) + if (_values == null) { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } - - v = CreatePriorityValue(property); - _values.Add(property, v); + _values = new ValueStore(this); } LogPropertySet(property, value, priority); - v.SetValue(value, (int)priority); + _values.AddValue(property, value, (int)priority); } /// 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/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 3a25407133..773049d3a5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; +using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; using Avalonia.Reactive; @@ -14,9 +16,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, - IDescription, - IObserver + public class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize @@ -55,7 +55,6 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private IDisposable _nodeSubscription; private object _root; private IDisposable _rootSubscription; private WeakReference _value; @@ -64,27 +63,22 @@ namespace Avalonia.Data.Core /// 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); } @@ -92,23 +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); + + _node = node; + Description = description; _root = rootObservable; } @@ -116,30 +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); + 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. /// @@ -202,34 +254,18 @@ namespace Avalonia.Data.Core } } - void IObserver.OnNext(object value) - { - var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; - broken?.Commit(Description); - _value = new WeakReference(value); - PublishNext(value); - } - - void IObserver.OnCompleted() - { - } - - void IObserver.OnError(Exception error) - { - } - protected override void Initialize() { _value = null; - _nodeSubscription = _node.Subscribe(this); + _node.Subscribe(ValueChanged); StartRoot(); } protected override void Deinitialize() { _rootSubscription?.Dispose(); - _nodeSubscription?.Dispose(); - _rootSubscription = _nodeSubscription = null; + _rootSubscription = null; + _node.Unsubscribe(); } protected override void Subscribed(IObserver observer, bool first) @@ -240,16 +276,9 @@ namespace Avalonia.Data.Core } } - private static ExpressionNode Parse(string expression, bool enableDataValidation) + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - if (!string.IsNullOrWhiteSpace(expression)) - { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); - } - else - { - return new EmptyExpressionNode(); - } + return ExpressionTreeParser.Parse(expression, enableDataValidation); } private void StartRoot() @@ -266,5 +295,13 @@ namespace Avalonia.Data.Core _node.Target = (WeakReference)_root; } } + + private void ValueChanged(object value) + { + 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/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 48edb218dc..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?.GetObservable(_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/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index aeec720920..5f63f6ef91 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -13,18 +13,19 @@ 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. diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 9b5318083a..c474f9098e 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -281,12 +281,12 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(this, old, Value)); + notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); } if (notification != null) { - Owner?.BindingNotificationReceived(this, notification); + Owner?.BindingNotificationReceived(Property, notification); } } else diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs new file mode 100644 index 0000000000..8283edab80 --- /dev/null +++ b/src/Avalonia.Base/ValueStore.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia +{ + internal class ValueStore : IPriorityValueOwner + { + private readonly AvaloniaObject _owner; + private readonly Dictionary _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) + { + ((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification); + } + + public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + { + ((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue); + } + + public IDictionary GetSetValues() => throw new NotImplementedException(); + + public object GetValue(AvaloniaProperty property) + { + var result = AvaloniaProperty.UnsetValue; + + if (_values.TryGetValue(property, out var value)) + { + result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; + } + + return result; + } + + public bool IsAnimating(AvaloniaProperty property) + { + return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false; + } + + public bool IsSet(AvaloniaProperty property) + { + if (_values.TryGetValue(property, out var value)) + { + return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; + } + + return false; + } + + public void Revalidate(AvaloniaProperty property) + { + if (_values.TryGetValue(property, out var value)) + { + (value as PriorityValue)?.Revalidate(); + } + } + + public void VerifyAccess() => _owner.VerifyAccess(); + + private PriorityValue CreatePriorityValue(AvaloniaProperty property) + { + var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); + Func validate2 = null; + + if (validate != null) + { + validate2 = v => validate(_owner, v); + } + + PriorityValue result = new PriorityValue( + this, + property, + property.PropertyType, + validate2); + + return result; + } + + private object Validate(AvaloniaProperty property, object value) + { + var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); + + if (validate != null && value != AvaloniaProperty.UnsetValue) + { + return validate(_owner, value); + } + + return value; + } + } +} 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/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/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/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/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/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2e668fda95..a7b8981583 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives var panel = (InputElement)Presenter.Panel; - foreach (var container in e.Containers) + if (panel != null) { - if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + foreach (var container in e.Containers) { - KeyboardNavigation.SetTabOnceActiveElement(panel, null); - break; + if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + { + KeyboardNavigation.SetTabOnceActiveElement(panel, null); + break; + } } } } 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/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index a6fe35d668..d404e6913b 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 @@ -152,6 +153,7 @@ namespace Avalonia.Controls double measuredWidth = 0; double measuredHeight = 0; double gap = Gap; + 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 ? gap : 0); measuredWidth = Math.Max(measuredWidth, size.Width); } else { - measuredWidth += size.Width + gap; + measuredWidth += size.Width + (child.IsVisible ? gap : 0); measuredHeight = Math.Max(measuredHeight, size.Height); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= gap; + measuredHeight -= (hasVisibleChild ? gap : 0); } else { - measuredWidth -= gap; + measuredWidth -= (hasVisibleChild ? gap : 0); } return new Size(measuredWidth, measuredHeight); @@ -193,6 +195,7 @@ namespace Avalonia.Controls double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; double gap = Gap; + 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 ? gap : 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 ? gap : 0); arrangedHeight = Math.Max(arrangedHeight, childHeight); } } if (orientation == Orientation.Vertical) { - arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); + arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height); } else { - arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); + arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width); } return new Size(arrangedWidth, arrangedHeight); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 2ea9319194..5b09fbfb51 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -557,7 +557,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/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..d1a023c42c 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,19 +302,17 @@ namespace Avalonia.Controls internal void Close(bool ignoreCancel) { - var cancelClosing = false; try { - cancelClosing = HandleClosing(); + if (!ignoreCancel && HandleClosing()) + { + return; + } } finally { - if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); - PlatformImpl?.Dispose(); - IsVisible = false; - } + PlatformImpl?.Dispose(); + HandleClosed(); } } @@ -313,6 +323,7 @@ namespace Avalonia.Controls { var args = new CancelEventArgs(); Closing?.Invoke(this, args); + return args.Cancel; } @@ -359,18 +370,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 +411,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 +524,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..c21a12f05b --- /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(); + } + } + + 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; + } + } + } +} \ No newline at end of file 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/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1a78e0f4d7..31b685f6b1 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -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.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..47b4528f88 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -45,7 +45,7 @@ - -