Browse Source

Merge branch 'master' into fixes/direct2d1-image-brush-null-source

pull/1686/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
e9638030f1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 156
      .editorconfig
  2. 8
      .gitignore
  3. 3
      Avalonia.sln
  4. 233
      build.cake
  5. 5
      build/System.Memory.props
  6. 92
      packages.cake
  7. 12
      parameters.cake
  8. 2
      readme.md
  9. 4
      samples/ControlCatalog/SideBar.xaml
  10. 4
      samples/RenderDemo/SideBar.xaml
  11. 139
      src/Avalonia.Base/AvaloniaObject.cs
  12. 60
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  13. 7
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  14. 120
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  15. 30
      src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs
  16. 165
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  17. 4
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  18. 71
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  19. 92
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  20. 2
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  21. 27
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  22. 219
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  23. 40
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  24. 10
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  25. 5
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  26. 14
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  27. 19
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  28. 16
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  29. 8
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  30. 68
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  31. 11
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  32. 31
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  33. 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  34. 27
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  35. 19
      src/Avalonia.Base/Data/Core/StreamNode.cs
  36. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  37. 4
      src/Avalonia.Base/PriorityValue.cs
  38. 172
      src/Avalonia.Base/ValueStore.cs
  39. 45
      src/Avalonia.Controls/AppBuilderBase.cs
  40. 112
      src/Avalonia.Controls/Application.cs
  41. 2
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  42. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
  43. 26
      src/Avalonia.Controls/ExitMode.cs
  44. 1
      src/Avalonia.Controls/ItemsControl.cs
  45. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  46. 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  47. 11
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  48. 1
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  49. 19
      src/Avalonia.Controls/StackPanel.cs
  50. 2
      src/Avalonia.Controls/TextBox.cs
  51. 18
      src/Avalonia.Controls/TopLevel.cs
  52. 67
      src/Avalonia.Controls/Window.cs
  53. 5
      src/Avalonia.Controls/WindowBase.cs
  54. 134
      src/Avalonia.Controls/WindowCollection.cs
  55. 5
      src/Avalonia.Layout/ILayoutRoot.cs
  56. 7
      src/Avalonia.Layout/LayoutManager.cs
  57. 10
      src/Avalonia.Layout/Layoutable.cs
  58. 2
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  59. 2
      src/Avalonia.Themes.Default/DropDown.xaml
  60. 9
      src/Avalonia.Themes.Default/MenuItem.xaml
  61. 4
      src/Avalonia.Themes.Default/ScrollBar.xaml
  62. 10
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  63. 7
      src/Avalonia.Themes.Default/Slider.xaml
  64. 4
      src/Avalonia.Themes.Default/TabControl.xaml
  65. 2
      src/Avalonia.Themes.Default/TextBox.xaml
  66. 4
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  67. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  68. 670
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  69. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  70. 2
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  71. 85
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  72. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs
  73. 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  74. 51
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  75. 8
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs
  76. 6
      src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs
  77. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  78. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  79. 35
      src/Markup/Avalonia.Markup/Data/Binding.cs
  80. 180
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  81. 3
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  82. 75
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  83. 35
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  84. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs
  85. 81
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  86. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs
  87. 4
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  88. 2
      src/OSX/Avalonia.MonoMac/KeyTransform.cs
  89. 10
      src/OSX/Avalonia.MonoMac/WindowImpl.cs
  90. 2
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  91. 3
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  92. 43
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  93. 30
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs
  94. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs
  95. 32
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  96. 224
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs
  97. 89
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  98. 17
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs
  99. 97
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs
  100. 24
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

156
.editorconfig

@ -1,11 +1,159 @@
; This file is for unifying the coding style for different editors and IDEs. # editorconfig.org
; More information at http://EditorConfig.org
# top-most EditorConfig file
root = true 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] [*.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 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

8
.gitignore

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

3
Avalonia.sln

@ -58,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9B9E
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\Shared\SharedAssemblyInfo.cs = src\Shared\SharedAssemblyInfo.cs src\Shared\SharedAssemblyInfo.cs = src\Shared\SharedAssemblyInfo.cs
EndProjectSection EndProjectSection
EndProject EndProject
@ -398,6 +399,7 @@ Global
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {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|iPhoneSimulator.Build.0 = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.ActiveCfg = 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.ActiveCfg = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.Build.0 = 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 {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.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = 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.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.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.Build.0 = 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 {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU

233
build.cake

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

5
build/System.Memory.props

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

92
packages.cake

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

12
parameters.cake

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

2
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 (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms! 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 (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). 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 <b>NuGet</b> 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)) Avalonia is delivered via <b>NuGet</b> 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: Use these commands in Package Manager console to install Avalonia manually:

4
samples/ControlCatalog/SideBar.xaml

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

4
samples/RenderDemo/SideBar.xaml

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

139
src/Avalonia.Base/AvaloniaObject.cs

@ -29,12 +29,6 @@ namespace Avalonia
/// </summary> /// </summary>
private IAvaloniaObject _inheritanceParent; private IAvaloniaObject _inheritanceParent;
/// <summary>
/// The set values/bindings on this object.
/// </summary>
private readonly Dictionary<AvaloniaProperty, PriorityValue> _values =
new Dictionary<AvaloniaProperty, PriorityValue>();
/// <summary> /// <summary>
/// Maintains a list of direct property binding subscriptions so that the binding source /// Maintains a list of direct property binding subscriptions so that the binding source
/// doesn't get collected. /// doesn't get collected.
@ -52,6 +46,7 @@ namespace Avalonia
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter; private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values;
/// <summary> /// <summary>
/// Delayed setter helper for direct properties. Used to fix #855. /// Delayed setter helper for direct properties. Used to fix #855.
@ -228,9 +223,20 @@ namespace Avalonia
{ {
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); 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 else
{ {
return GetValueInternal(property); return GetDefaultValue(property);
} }
} }
@ -257,7 +263,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; return _values?.IsAnimating(property) ?? false;
} }
/// <summary> /// <summary>
@ -274,14 +280,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
PriorityValue value; return _values?.IsSet(property) ?? false;
if (_values.TryGetValue(property, out value))
{
return value.Value != AvaloniaProperty.UnsetValue;
}
return false;
} }
/// <summary> /// <summary>
@ -369,14 +368,6 @@ namespace Avalonia
} }
else else
{ {
PriorityValue v;
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
}
Logger.Verbose( Logger.Verbose(
LogArea.Property, LogArea.Property,
this, this,
@ -385,7 +376,12 @@ namespace Avalonia
description, description,
priority); 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) public void Revalidate(AvaloniaProperty property)
{ {
VerifyAccess(); VerifyAccess();
PriorityValue value; _values?.Revalidate(property);
if (_values.TryGetValue(property, out value))
{
value.Revalidate();
}
} }
/// <inheritdoc/> /// <inheritdoc/>
void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue) void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{ {
var property = sender.Property;
var priority = (BindingPriority)sender.ValuePriority;
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) : GetDefaultValue(property) :
oldValue; oldValue;
@ -439,7 +427,7 @@ namespace Avalonia
if (!Equals(oldValue, newValue)) if (!Equals(oldValue, newValue))
{ {
RaisePropertyChanged(property, oldValue, newValue, priority); RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
Logger.Verbose( Logger.Verbose(
LogArea.Property, LogArea.Property,
@ -448,14 +436,14 @@ namespace Avalonia
property, property,
oldValue, oldValue,
newValue, newValue,
priority); (BindingPriority)priority);
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification) void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{ {
UpdateDataValidation(sender.Property, notification); UpdateDataValidation(property, notification);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -468,10 +456,7 @@ namespace Avalonia
/// Gets all priority values set on the object. /// Gets all priority values set on the object.
/// </summary> /// </summary>
/// <returns>A collection of property/value tuples.</returns> /// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
{
return _values;
}
/// <summary> /// <summary>
/// Forces revalidation of properties when a property value changes. /// Forces revalidation of properties when a property value changes.
@ -660,68 +645,18 @@ namespace Avalonia
} }
} }
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The <see cref="PriorityValue"/>.</returns>
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType());
Func<object, object> validate2 = null;
if (validate != null)
{
validate2 = v => validate(this, v);
}
PriorityValue result = new PriorityValue(
this,
property,
property.PropertyType,
validate2);
return result;
}
/// <summary> /// <summary>
/// Gets the default value for a property. /// Gets the default value for a property.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The default value.</returns> /// <returns>The default value.</returns>
private object GetDefaultValue(AvaloniaProperty property) internal object GetDefaultValue(AvaloniaProperty property)
{ {
if (property.Inherits && InheritanceParent is AvaloniaObject aobj) if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValueInternal(property); return aobj.GetValue(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
} }
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value
/// without check for registered as this can slow getting the value
/// this method is intended for internal usage in AvaloniaObject only
/// it's called only after check the property is registered
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
private object GetValueInternal(AvaloniaProperty property)
{
object result = AvaloniaProperty.UnsetValue;
PriorityValue value;
if (_values.TryGetValue(property, out value))
{
result = value.Value;
}
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
/// <summary> /// <summary>
/// Sets the value of a direct property. /// Sets the value of a direct property.
/// </summary> /// </summary>
@ -802,21 +737,13 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)")); originalValue?.GetType().FullName ?? "(null)"));
} }
PriorityValue v; if (_values == null)
if (!_values.TryGetValue(property, out v))
{ {
if (value == AvaloniaProperty.UnsetValue) _values = new ValueStore(this);
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
} }
LogPropertySet(property, value, priority); LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority); _values.AddValue(property, value, (int)priority);
} }
/// <summary> /// <summary>

60
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<object>(obj, _property).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

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

@ -6,13 +6,8 @@ using System.Reactive.Linq;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class EmptyExpressionNode : ExpressionNode public class EmptyExpressionNode : ExpressionNode
{ {
public override string Description => "."; public override string Description => ".";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
return Observable.Return(reference.Target);
}
} }
} }

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

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

30
src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs

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

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

@ -3,9 +3,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive; using Avalonia.Reactive;
@ -14,9 +16,7 @@ namespace Avalonia.Data.Core
/// <summary> /// <summary>
/// Observes and sets the value of an expression on an object. /// Observes and sets the value of an expression on an object.
/// </summary> /// </summary>
public class ExpressionObserver : LightweightObservableBase<object>, public class ExpressionObserver : LightweightObservableBase<object>, IDescription
IDescription,
IObserver<object>
{ {
/// <summary> /// <summary>
/// An ordered collection of property accessor plugins that can be used to customize /// 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 static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private IDisposable _nodeSubscription;
private object _root; private object _root;
private IDisposable _rootSubscription; private IDisposable _rootSubscription;
private WeakReference<object> _value; private WeakReference<object> _value;
@ -64,27 +63,22 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="root">The root object.</param> /// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
object root, object root,
string expression, ExpressionNode node,
bool enableDataValidation = false,
string description = null) string description = null)
{ {
Contract.Requires<ArgumentNullException>(expression != null);
if (root == AvaloniaProperty.UnsetValue) if (root == AvaloniaProperty.UnsetValue)
{ {
root = null; root = null;
} }
Expression = expression; _node = node;
Description = description ?? expression; Description = description;
_node = Parse(expression, enableDataValidation);
_root = new WeakReference(root); _root = new WeakReference(root);
} }
@ -92,23 +86,19 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param> /// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
IObservable<object> rootObservable, IObservable<object> rootObservable,
string expression, ExpressionNode node,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootObservable != null); Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_node = node;
Expression = expression; Description = description;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_root = rootObservable; _root = rootObservable;
} }
@ -116,30 +106,92 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootGetter">A function which gets the root object.</param> /// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param> /// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
Func<object> rootGetter, Func<object> rootGetter,
string expression, ExpressionNode node,
IObservable<Unit> update, IObservable<Unit> update,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootGetter != null); Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null); Contract.Requires<ArgumentNullException>(update != null);
Description = description;
Expression = expression; _node = node;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_node.Target = new WeakReference(rootGetter()); _node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter()); _root = update.Select(x => rootGetter());
} }
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
T root,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable.Select(o => (object)o),
Parse(expression, enableDataValidation),
description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation),
update,
description ?? expression.ToString());
}
/// <summary> /// <summary>
/// Attempts to set the value of a property expression. /// Attempts to set the value of a property expression.
/// </summary> /// </summary>
@ -202,34 +254,18 @@ namespace Avalonia.Data.Core
} }
} }
void IObserver<object>.OnNext(object value)
{
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description);
_value = new WeakReference<object>(value);
PublishNext(value);
}
void IObserver<object>.OnCompleted()
{
}
void IObserver<object>.OnError(Exception error)
{
}
protected override void Initialize() protected override void Initialize()
{ {
_value = null; _value = null;
_nodeSubscription = _node.Subscribe(this); _node.Subscribe(ValueChanged);
StartRoot(); StartRoot();
} }
protected override void Deinitialize() protected override void Deinitialize()
{ {
_rootSubscription?.Dispose(); _rootSubscription?.Dispose();
_nodeSubscription?.Dispose(); _rootSubscription = null;
_rootSubscription = _nodeSubscription = null; _node.Unsubscribe();
} }
protected override void Subscribed(IObserver<object> observer, bool first) protected override void Subscribed(IObserver<object> 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 ExpressionTreeParser.Parse(expression, enableDataValidation);
{
return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
else
{
return new EmptyExpressionNode();
}
} }
private void StartRoot() private void StartRoot()
@ -266,5 +295,13 @@ namespace Avalonia.Data.Core
_node.Target = (WeakReference)_root; _node.Target = (WeakReference)_root;
} }
} }
private void ValueChanged(object value)
{
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description);
_value = new WeakReference<object>(value);
PublishNext(value);
}
} }
} }

4
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

@ -17,8 +17,8 @@ namespace Avalonia.Data.Core
/// </summary> /// </summary>
/// <param name="column">The column position of the error.</param> /// <param name="column">The column position of the error.</param>
/// <param name="message">The exception message.</param> /// <param name="message">The exception message.</param>
public ExpressionParseException(int column, string message) public ExpressionParseException(int column, string message, Exception innerException = null)
: base(message) : base(message, innerException)
{ {
Column = column; Column = column;
} }

71
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?;
}
}

92
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<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
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);
}
}

2
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

@ -7,7 +7,7 @@ using Avalonia.Data;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class LogicalNotNode : ExpressionNode, ITransformNode public class LogicalNotNode : ExpressionNode, ITransformNode
{ {
public override string Description => "!"; public override string Description => "!";

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

219
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<ExpressionNode> Nodes { get; }
public ExpressionVisitorNodeBuilder(bool enableDataValidation)
{
_enableDataValidation = enableDataValidation;
Nodes = new List<ExpressionNode>();
}
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<AvaloniaProperty>(node.Arguments[0]);
Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation));
}
else
{
Nodes.Add(new IndexerExpressionNode(node));
}
return node;
}
private T GetArgumentExpressionValue<T>(Expression expr)
{
try
{
return Expression.Lambda<Func<T>>(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<MethodInfo>(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}.");
}
}
}

40
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) private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
{ {
if (!propertyName.Contains(".")) return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
{
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;
} }
private static bool IsOfType(Type type, string typeName) private static bool IsOfType(Type type, string typeName)
@ -145,15 +117,15 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{ {
_subscription?.Dispose(); _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
_subscription = null;
} }
protected override void SubscribeCore(IObserver<object> observer) protected override void UnsubscribeCore()
{ {
_subscription = Instance?.GetObservable(_property).Subscribe(observer); _subscription?.Dispose();
_subscription = null;
} }
} }
} }

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

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

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

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data;
using System; using System;
using System.Reflection; using System.Reflection;
@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
} }
catch (Exception ex) catch (Exception ex)
{ {
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError));
} }
return false; return false;

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -3,14 +3,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class PropertyAccessorNode : SettableNode public class PropertyAccessorNode : SettableNode
{ {
private readonly bool _enableValidation; private readonly bool _enableValidation;
private IPropertyAccessor _accessor; private IPropertyAccessor _accessor;
@ -39,7 +37,7 @@ namespace Avalonia.Data.Core
return false; return false;
} }
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
var accessor = plugin?.Start(reference, 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. if (accessor == null)
return Observable.Using( {
() => throw new NotSupportedException(
{ $"Could not find a matching property accessor for {PropertyName}.");
_accessor = accessor; }
return Disposable.Create(() =>
{ accessor.Subscribe(ValueChanged);
_accessor = null; _accessor = accessor;
}); }
},
_ => accessor); protected override void StopListeningCore()
{
_accessor.Dispose();
_accessor = null;
} }
} }
} }

2
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal abstract class SettableNode : ExpressionNode public abstract class SettableNode : ExpressionNode
{ {
public bool SetTargetValue(object value, BindingPriority priority) public bool SetTargetValue(object value, BindingPriority priority)
{ {

27
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<T>(this Task<T> @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<T>(this IObservable<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
}
}

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

9
src/Avalonia.Base/IPriorityValueOwner.cs

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

4
src/Avalonia.Base/PriorityValue.cs

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

172
src/Avalonia.Base/ValueStore.cs

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

45
src/Avalonia.Controls/AppBuilderBase.cs

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

112
src/Avalonia.Controls/Application.cs

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

2
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -28,7 +28,7 @@ namespace Avalonia.Controls.Embedding
{ {
EnsureInitialized(); EnsureInitialized();
ApplyTemplate(); ApplyTemplate();
LayoutManager.Instance.ExecuteInitialLayoutPass(this); LayoutManager.ExecuteInitialLayoutPass(this);
} }
private void EnsureInitialized() private void EnsureInitialized()

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
{ {
EnsureInitialized(); EnsureInitialized();
ApplyTemplate(); ApplyTemplate();
LayoutManager.Instance.ExecuteInitialLayoutPass(this); LayoutManager.ExecuteInitialLayoutPass(this);
} }
private void EnsureInitialized() private void EnsureInitialized()

26
src/Avalonia.Controls/ExitMode.cs

@ -0,0 +1,26 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia
{
/// <summary>
/// Enum for ExitMode
/// </summary>
public enum ExitMode
{
/// <summary>
/// Indicates an implicit call to Application.Exit when the last window closes.
/// </summary>
OnLastWindowClose,
/// <summary>
/// Indicates an implicit call to Application.Exit when the main window closes.
/// </summary>
OnMainWindowClose,
/// <summary>
/// Indicates that the application only exits on an explicit call to Application.Exit.
/// </summary>
OnExplicitExit
}
}

1
src/Avalonia.Controls/ItemsControl.cs

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

2
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -518,7 +518,7 @@ namespace Avalonia.Controls.Presenters
} }
var container = generator.ContainerFromIndex(index); 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 // 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 // is only partially visible due to differing item sizes. If the container is only

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

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

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

@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives
var panel = (InputElement)Presenter.Panel; 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); if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
break; {
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
} }
} }
} }

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

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

19
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Linq;
using Avalonia.Input; using Avalonia.Input;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -152,6 +153,7 @@ namespace Avalonia.Controls
double measuredWidth = 0; double measuredWidth = 0;
double measuredHeight = 0; double measuredHeight = 0;
double gap = Gap; double gap = Gap;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
foreach (Control child in Children) foreach (Control child in Children)
{ {
@ -160,23 +162,23 @@ namespace Avalonia.Controls
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
measuredHeight += size.Height + gap; measuredHeight += size.Height + (child.IsVisible ? gap : 0);
measuredWidth = Math.Max(measuredWidth, size.Width); measuredWidth = Math.Max(measuredWidth, size.Width);
} }
else else
{ {
measuredWidth += size.Width + gap; measuredWidth += size.Width + (child.IsVisible ? gap : 0);
measuredHeight = Math.Max(measuredHeight, size.Height); measuredHeight = Math.Max(measuredHeight, size.Height);
} }
} }
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
measuredHeight -= gap; measuredHeight -= (hasVisibleChild ? gap : 0);
} }
else else
{ {
measuredWidth -= gap; measuredWidth -= (hasVisibleChild ? gap : 0);
} }
return new Size(measuredWidth, measuredHeight); return new Size(measuredWidth, measuredHeight);
@ -193,6 +195,7 @@ namespace Avalonia.Controls
double arrangedWidth = finalSize.Width; double arrangedWidth = finalSize.Width;
double arrangedHeight = finalSize.Height; double arrangedHeight = finalSize.Height;
double gap = Gap; double gap = Gap;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
@ -214,25 +217,25 @@ namespace Avalonia.Controls
Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
ArrangeChild(child, childFinal, finalSize, orientation); ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth = Math.Max(arrangedWidth, childWidth); arrangedWidth = Math.Max(arrangedWidth, childWidth);
arrangedHeight += childHeight + gap; arrangedHeight += childHeight + (child.IsVisible ? gap : 0);
} }
else else
{ {
double height = Math.Max(childHeight, arrangedHeight); double height = Math.Max(childHeight, arrangedHeight);
Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
ArrangeChild(child, childFinal, finalSize, orientation); ArrangeChild(child, childFinal, finalSize, orientation);
arrangedWidth += childWidth + gap; arrangedWidth += childWidth + (child.IsVisible ? gap : 0);
arrangedHeight = Math.Max(arrangedHeight, childHeight); arrangedHeight = Math.Max(arrangedHeight, childHeight);
} }
} }
if (orientation == Orientation.Vertical) if (orientation == Orientation.Vertical)
{ {
arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height);
} }
else else
{ {
arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width);
} }
return new Size(arrangedWidth, arrangedHeight); return new Size(arrangedWidth, arrangedHeight);

2
src/Avalonia.Controls/TextBox.cs

@ -557,7 +557,7 @@ namespace Avalonia.Controls
var index = CaretIndex = _presenter.GetCaretIndex(point); var index = CaretIndex = _presenter.GetCaretIndex(point);
var text = Text; var text = Text;
if (text != null) if (text != null && e.MouseButton == MouseButton.Left)
{ {
switch (e.ClickCount) switch (e.ClickCount)
{ {

18
src/Avalonia.Controls/TopLevel.cs

@ -54,6 +54,7 @@ namespace Avalonia.Controls
private readonly IApplicationLifecycle _applicationLifecycle; private readonly IApplicationLifecycle _applicationLifecycle;
private readonly IPlatformRenderInterface _renderInterface; private readonly IPlatformRenderInterface _renderInterface;
private Size _clientSize; private Size _clientSize;
private ILayoutManager _layoutManager;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class. /// Initializes static members of the <see cref="TopLevel"/> class.
@ -147,6 +148,16 @@ namespace Avalonia.Controls
protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); }
} }
public ILayoutManager LayoutManager
{
get
{
if (_layoutManager == null)
_layoutManager = CreateLayoutManager();
return _layoutManager;
}
}
/// <summary> /// <summary>
/// Gets the platform-specific window implementation. /// Gets the platform-specific window implementation.
/// </summary> /// </summary>
@ -235,6 +246,11 @@ namespace Avalonia.Controls
{ {
return PlatformImpl?.PointToScreen(p) ?? default(Point); return PlatformImpl?.PointToScreen(p) ?? default(Point);
} }
/// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary>
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager();
/// <summary> /// <summary>
/// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>. /// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
@ -267,7 +283,7 @@ namespace Avalonia.Controls
ClientSize = clientSize; ClientSize = clientSize;
Width = clientSize.Width; Width = clientSize.Width;
Height = clientSize.Height; Height = clientSize.Height;
LayoutManager.Instance.ExecuteLayoutPass(); LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize); Renderer?.Resized(clientSize);
} }

67
src/Avalonia.Controls/Window.cs

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

5
src/Avalonia.Controls/WindowBase.cs

@ -179,10 +179,9 @@ namespace Avalonia.Controls
if (!_hasExecutedInitialLayoutPass) if (!_hasExecutedInitialLayoutPass)
{ {
LayoutManager.Instance.ExecuteInitialLayoutPass(this); LayoutManager.ExecuteInitialLayoutPass(this);
_hasExecutedInitialLayoutPass = true; _hasExecutedInitialLayoutPass = true;
} }
PlatformImpl?.Show(); PlatformImpl?.Show();
Renderer?.Start(); Renderer?.Start();
} }
@ -262,7 +261,7 @@ namespace Avalonia.Controls
Height = clientSize.Height; Height = clientSize.Height;
} }
ClientSize = clientSize; ClientSize = clientSize;
LayoutManager.Instance.ExecuteLayoutPass(); LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize); Renderer?.Resized(clientSize);
} }

134
src/Avalonia.Controls/WindowCollection.cs

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

5
src/Avalonia.Layout/ILayoutRoot.cs

@ -22,5 +22,10 @@ namespace Avalonia.Layout
/// The scaling factor to use in layout. /// The scaling factor to use in layout.
/// </summary> /// </summary>
double LayoutScaling { get; } double LayoutScaling { get; }
/// <summary>
/// Associated instance of layout manager
/// </summary>
ILayoutManager LayoutManager { get; }
} }
} }

7
src/Avalonia.Layout/LayoutManager.cs

@ -19,11 +19,6 @@ namespace Avalonia.Layout
private bool _queued; private bool _queued;
private bool _running; private bool _running;
/// <summary>
/// Gets the layout manager.
/// </summary>
public static ILayoutManager Instance => AvaloniaLocator.Current.GetService<ILayoutManager>();
/// <inheritdoc/> /// <inheritdoc/>
public void InvalidateMeasure(ILayoutable control) public void InvalidateMeasure(ILayoutable control)
{ {
@ -170,7 +165,7 @@ namespace Avalonia.Layout
{ {
root.Measure(Size.Infinity); root.Measure(Size.Infinity);
} }
else else if (control.PreviousMeasure.HasValue)
{ {
control.Measure(control.PreviousMeasure.Value); control.Measure(control.PreviousMeasure.Value);
} }

10
src/Avalonia.Layout/Layoutable.cs

@ -389,7 +389,7 @@ namespace Avalonia.Layout
if (((ILayoutable)this).IsAttachedToVisualTree) if (((ILayoutable)this).IsAttachedToVisualTree)
{ {
LayoutManager.Instance?.InvalidateMeasure(this); (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this);
InvalidateVisual(); InvalidateVisual();
} }
OnMeasureInvalidated(); OnMeasureInvalidated();
@ -406,12 +406,8 @@ namespace Avalonia.Layout
Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); Logger.Verbose(LogArea.Layout, this, "Invalidated arrange");
IsArrangeValid = false; IsArrangeValid = false;
(VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this);
if (((ILayoutable)this).IsAttachedToVisualTree) InvalidateVisual();
{
LayoutManager.Instance?.InvalidateArrange(this);
InvalidateVisual();
}
} }
} }

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

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

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

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

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

@ -45,7 +45,7 @@
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
PlacementMode="Right" PlacementMode="Right"
StaysOpen="True" StaysOpen="True"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}" IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
ObeyScreenEdges="True"> ObeyScreenEdges="True">
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
@ -92,7 +92,7 @@
</ContentPresenter.DataTemplates> </ContentPresenter.DataTemplates>
</ContentPresenter> </ContentPresenter>
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}" IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True" StaysOpen="True"
ObeyScreenEdges="True"> ObeyScreenEdges="True">
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
@ -122,11 +122,6 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root"> <Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs

@ -19,7 +19,7 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{ {
var parser = new SelectorParser((t, ns) => context.ResolveType(ns, t)); var parser = new SelectorParser(context.ResolveType);
return parser.Parse((string)value); return parser.Parse((string)value);
} }

1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -37,6 +37,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return new Binding return new Binding
{ {
TypeResolver = descriptorContext.ResolveType,
Converter = Converter, Converter = Converter,
ConverterParameter = ConverterParameter, ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName, ElementName = pathInfo.ElementName ?? ElementName,

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

@ -1,51 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data;
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
using System;
using Avalonia.Data.Converters;
using Avalonia.Markup.Data;
using Portable.Xaml.Markup;
[MarkupExtensionReturnType(typeof(IBinding))]
public class TemplateBindingExtension : MarkupExtension
{
public TemplateBindingExtension()
{
}
public TemplateBindingExtension(string path)
{
Path = path;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new Binding
{
Converter = Converter,
ElementName = ElementName,
Mode = Mode,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Path = Path ?? string.Empty,
Priority = Priority,
};
}
public IValueConverter Converter { get; set; }
public string ElementName { get; set; }
public object FallbackValue { get; set; }
public BindingMode Mode { get; set; }
[ConstructorArgument("path")]
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.TemplatedParent;
}
}

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

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

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

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -14,7 +15,8 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent] [TemplateContent]
public object Content { get; set; } public object Content { get; set; }
public IControl Build(ITemplatedControl control) public Type TargetType { get; set; }
=> TemplateContent.Load(Content);
public IControl Build(ITemplatedControl control) => TemplateContent.Load(Content);
} }
} }

3
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -4,6 +4,7 @@
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
@ -37,7 +38,7 @@ namespace Avalonia.Markup.Xaml.Templates
return o; return o;
} }
var expression = new ExpressionObserver(o, MemberName); var expression = ExpressionObserverBuilder.Build(o, MemberName);
object result = AvaloniaProperty.UnsetValue; object result = AvaloniaProperty.UnsetValue;
expression.Subscribe(x => result = x); expression.Subscribe(x => result = x);

3
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Markup.Parsers;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates namespace Avalonia.Markup.Xaml.Templates
@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Templates
{ {
if (ItemsSource != null) if (ItemsSource != null)
{ {
var obs = new ExpressionObserver(item, ItemsSource.Path); var obs = ExpressionObserverBuilder.Build(item, ItemsSource.Path);
return InstancedBinding.OneWay(obs, BindingPriority.Style); return InstancedBinding.OneWay(obs, BindingPriority.Style);
} }

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

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Parsers;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -85,6 +86,11 @@ namespace Avalonia.Data
public WeakReference DefaultAnchor { get; set; } public WeakReference DefaultAnchor { get; set; }
/// <summary>
/// Gets or sets a function used to resolve types from names in the binding path.
/// </summary>
public Func<string, string, Type> TypeResolver { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public InstancedBinding Initiate( public InstancedBinding Initiate(
IAvaloniaObject target, IAvaloniaObject target,
@ -189,20 +195,22 @@ namespace Avalonia.Data
if (!targetIsDataContext) if (!targetIsDataContext)
{ {
var result = new ExpressionObserver( var result = ExpressionObserverBuilder.Build(
() => target.GetValue(StyledElement.DataContextProperty), () => target.GetValue(StyledElement.DataContextProperty),
path, path,
new UpdateSignal(target, StyledElement.DataContextProperty), new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation); enableDataValidation,
typeResolver: TypeResolver);
return result; return result;
} }
else else
{ {
return new ExpressionObserver( return ExpressionObserverBuilder.Build(
GetParentDataContext(target), GetParentDataContext(target),
path, path,
enableDataValidation); enableDataValidation,
typeResolver: TypeResolver);
} }
} }
@ -215,11 +223,12 @@ namespace Avalonia.Data
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}"; var description = $"#{elementName}.{path}";
var result = new ExpressionObserver( var result = ExpressionObserverBuilder.Build(
ControlLocator.Track(target, elementName), ControlLocator.Track(target, elementName),
path, path,
enableDataValidation, enableDataValidation,
description); description,
typeResolver: TypeResolver);
return result; return result;
} }
@ -251,10 +260,11 @@ namespace Avalonia.Data
throw new InvalidOperationException("Invalid tree to traverse."); throw new InvalidOperationException("Invalid tree to traverse.");
} }
return new ExpressionObserver( return ExpressionObserverBuilder.Build(
controlLocator, controlLocator,
path, path,
enableDataValidation); enableDataValidation,
typeResolver: TypeResolver);
} }
private ExpressionObserver CreateSourceObserver( private ExpressionObserver CreateSourceObserver(
@ -264,7 +274,7 @@ namespace Avalonia.Data
{ {
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, enableDataValidation); return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
} }
private ExpressionObserver CreateTemplatedParentObserver( private ExpressionObserver CreateTemplatedParentObserver(
@ -273,12 +283,13 @@ namespace Avalonia.Data
bool enableDataValidation) bool enableDataValidation)
{ {
Contract.Requires<ArgumentNullException>(target != null); Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver( var result = ExpressionObserverBuilder.Build(
() => target.GetValue(StyledElement.TemplatedParentProperty), () => target.GetValue(StyledElement.TemplatedParentProperty),
path, path,
new UpdateSignal(target, StyledElement.TemplatedParentProperty), new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation); enableDataValidation,
typeResolver: TypeResolver);
return result; return result;
} }

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

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

3
src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -1,11 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Markup.Parsers
{ {
internal static class ArgumentListParser internal static class ArgumentListParser
{ {

75
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@ -0,0 +1,75 @@
using Avalonia.Data.Core;
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Text;
namespace Avalonia.Markup.Parsers
{
public static class ExpressionObserverBuilder
{
internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{
if (string.IsNullOrWhiteSpace(expression))
{
return new EmptyExpressionNode();
}
var reader = new Reader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader.Position, "Expected end of expression.");
}
return node;
}
public static ExpressionObserver Build(
object root,
string expression,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
return new ExpressionObserver(
root,
Parse(expression, enableDataValidation, typeResolver),
description ?? expression);
}
public static ExpressionObserver Build(
IObservable<object> rootObservable,
string expression,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable,
Parse(expression, enableDataValidation, typeResolver),
description ?? expression);
}
public static ExpressionObserver Build(
Func<object> rootGetter,
string expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null,
Func<string, string, Type> typeResolver = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation, typeResolver),
update,
description ?? expression);
}
}
}

35
src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -1,18 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers.Nodes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Markup.Parsers
{ {
internal class ExpressionParser internal class ExpressionParser
{ {
private bool _enableValidation; private readonly bool _enableValidation;
private readonly Func<string, string, Type> _typeResolver;
public ExpressionParser(bool enableValidation) public ExpressionParser(bool enableValidation, Func<string, string, Type> typeResolver)
{ {
_typeResolver = typeResolver;
_enableValidation = enableValidation; _enableValidation = enableValidation;
} }
@ -130,7 +134,19 @@ namespace Avalonia.Data.Core.Parsers
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes) private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{ {
var owner = IdentifierParser.Parse(r); string ns = string.Empty;
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
if (r.End || !r.TakeIf('.')) if (r.End || !r.TakeIf('.'))
{ {
@ -144,7 +160,14 @@ namespace Avalonia.Data.Core.Parsers
throw new ExpressionParseException(r.Position, "Expected ')'."); throw new ExpressionParseException(r.Position, "Expected ')'.");
} }
nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); if (_typeResolver == null)
{
throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?");
}
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name);
nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation));
return State.AfterMember; return State.AfterMember;
} }
@ -157,7 +180,7 @@ namespace Avalonia.Data.Core.Parsers
throw new ExpressionParseException(r.Position, "Indexer may not be empty."); throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
} }
nodes.Add(new IndexerNode(args)); nodes.Add(new StringIndexerNode(args));
return State.AfterMember; return State.AfterMember;
} }

2
src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs

@ -4,7 +4,7 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Markup.Parsers
{ {
internal static class IdentifierParser internal static class IdentifierParser
{ {

81
src/Avalonia.Base/Data/Core/IndexerNode.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@ -12,46 +12,19 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
namespace Avalonia.Data.Core namespace Avalonia.Markup.Parsers.Nodes
{ {
internal class IndexerNode : SettableNode internal class StringIndexerNode : IndexerNodeBase
{ {
public IndexerNode(IList<string> arguments) public StringIndexerNode(IList<string> arguments)
{ {
Arguments = arguments; Arguments = arguments;
} }
public override string Description => "[" + string.Join(",", Arguments) + "]"; public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
return Observable.Merge(inputs).StartWith(GetValue(target));
}
protected override bool SetTargetValueCore(object value, BindingPriority priority) protected override bool SetTargetValueCore(object value, BindingPriority priority)
{ {
var typeInfo = Target.Target.GetType().GetTypeInfo(); var typeInfo = Target.Target.GetType().GetTypeInfo();
@ -156,7 +129,7 @@ namespace Avalonia.Data.Core
public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
private object GetValue(object target) protected override object GetValue(object target)
{ {
var typeInfo = target.GetType().GetTypeInfo(); var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList; var list = target as IList;
@ -309,45 +282,19 @@ namespace Avalonia.Data.Core
} }
} }
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{ {
if (sender is IList) var typeInfo = sender.GetType().GetTypeInfo();
{ return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return false;
}
var index = (int)indexObject;
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
} }
private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) protected override int? TryGetFirstArgumentAsInt()
{ {
var typeInfo = sender.GetType().GetTypeInfo(); if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; {
return (int?)value;
}
return null;
} }
} }
} }

2
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs

@ -3,7 +3,7 @@
using System; using System;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Markup.Parsers
{ {
internal class Reader internal class Reader
{ {

4
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -52,11 +52,11 @@ namespace Avalonia.Markup.Parsers
if (ofType != null) if (ofType != null)
{ {
result = result.OfType(_typeResolver(ofType.TypeName, ofType.Xmlns)); result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName));
} }
if (@is != null) if (@is != null)
{ {
result = result.Is(_typeResolver(@is.TypeName, @is.Xmlns)); result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName));
} }
else if (@class != null) else if (@class != null)
{ {

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

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

10
src/OSX/Avalonia.MonoMac/WindowImpl.cs

@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Platform; using Avalonia.Platform;
using MonoMac.AppKit; using MonoMac.AppKit;
using MonoMac.CoreGraphics; using MonoMac.CoreGraphics;
using Avalonia.Threading;
namespace Avalonia.MonoMac namespace Avalonia.MonoMac
{ {
@ -16,7 +17,14 @@ namespace Avalonia.MonoMac
public WindowImpl() public WindowImpl()
{ {
UpdateStyle(); // Post UpdateStyle to UIThread otherwise for as yet unknown reason.
// The window becomes transparent to mouse clicks except a 100x100 square
// at the top left. (danwalmsley)
Dispatcher.UIThread.Post(() =>
{
UpdateStyle();
});
Window.SetCanBecomeKeyAndMain(); Window.SetCanBecomeKeyAndMain();
Window.DidResize += delegate Window.DidResize += delegate

2
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@ -49,7 +49,7 @@ namespace Avalonia.Win32.Interop.Wpf
protected override void HandleResized(Size clientSize) protected override void HandleResized(Size clientSize)
{ {
ClientSize = clientSize; ClientSize = clientSize;
LayoutManager.Instance.ExecuteLayoutPass(); LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize); Renderer?.Resized(clientSize);
} }

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

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

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

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value() public async Task Should_Get_Simple_Property_Value()
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Simple_Property_Value() public void Should_Set_Simple_Property_Value()
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
target.OnNext("bar"); target.OnNext("bar");
@ -47,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Indexed_Value() public void Should_Set_Indexed_Value()
{ {
var data = new { Foo = new[] { "foo" } }; var data = new { Foo = new[] { "foo" } };
var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string));
target.OnNext("bar"); target.OnNext("bar");
@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_Get_String_To_Double() public async Task Should_Convert_Get_String_To_Double()
{ {
var data = new Class1 { StringValue = $"{5.6}" }; var data = new Class1 { StringValue = $"{5.6}" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(5.6, result); Assert.Equal(5.6, result);
@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Getting_Invalid_Double_String_Should_Return_BindingError() public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1); var result = await target.Take(1);
Assert.IsType<BindingNotification>(result); Assert.IsType<BindingNotification>(result);
@ -84,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{ {
var data = new Class1 { StringValue = null }; var data = new Class1 { StringValue = null };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result); Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -96,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Convert_Set_String_To_Double() public void Should_Convert_Set_String_To_Double()
{ {
var data = new Class1 { StringValue = $"{5.6}" }; var data = new Class1 { StringValue = $"{5.6}" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
target.OnNext(6.7); target.OnNext(6.7);
@ -109,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_Get_Double_To_String() public async Task Should_Convert_Get_Double_To_String()
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal($"{5.6}", result); Assert.Equal($"{5.6}", result);
@ -121,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Convert_Set_Double_To_String() public void Should_Convert_Set_Double_To_String()
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext($"{6.7}"); target.OnNext($"{6.7}");
@ -135,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"), ExpressionObserver.Create(data, o => o.StringValue),
typeof(int), typeof(int),
42, 42,
DefaultValueConverter.Instance); DefaultValueConverter.Instance);
@ -156,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true), ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int), typeof(int),
42, 42,
DefaultValueConverter.Instance); DefaultValueConverter.Instance);
@ -177,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"), ExpressionObserver.Create(data, o => o.StringValue),
typeof(int), typeof(int),
"bar", "bar",
DefaultValueConverter.Instance); DefaultValueConverter.Instance);
@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true), ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int), typeof(int),
"bar", "bar",
DefaultValueConverter.Instance); DefaultValueConverter.Instance);
@ -220,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Setting_Invalid_Double_String_Should_Not_Change_Target() public void Setting_Invalid_Double_String_Should_Not_Change_Target()
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext("foo"); target.OnNext("foo");
@ -234,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"), ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string), typeof(string),
"9.8", "9.8",
DefaultValueConverter.Instance); DefaultValueConverter.Instance);
@ -250,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Coerce_Setting_Null_Double_To_Default_Value() public void Should_Coerce_Setting_Null_Double_To_Default_Value()
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext(null); target.OnNext(null);
@ -263,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
target.OnNext(AvaloniaProperty.UnsetValue); target.OnNext(AvaloniaProperty.UnsetValue);
@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var converter = new Mock<IValueConverter>(); var converter = new Mock<IValueConverter>();
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"), ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string), typeof(string),
converter.Object, converter.Object,
converterParameter: "foo"); converterParameter: "foo");
@ -297,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>(); var converter = new Mock<IValueConverter>();
var target = new BindingExpression( var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"), ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string), typeof(string),
converter.Object, converter.Object,
converterParameter: "foo"); converterParameter: "foo");
@ -314,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>(); var converter = new Mock<IValueConverter>();
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string));
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.Subscribe(x => result.Add(x));
@ -341,7 +342,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Second_Subscription_Should_Fire_Immediately() public void Second_Subscription_Should_Fire_Immediately()
{ {
var data = new Class1 { StringValue = "foo" }; var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
object result = null; object result = null;
target.Subscribe(); target.Subscribe();

30
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs

@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
public class ExpressionObserverTests_AttachedProperty public class ExpressionObserverTests_AttachedProperty
{ {
public ExpressionObserverTests_AttachedProperty()
{
var foo = Owner.FooProperty;
}
[Fact] [Fact]
public async Task Should_Get_Attached_Property_Value() public async Task Should_Get_Attached_Property_Value()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "(Owner.Foo)"); var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -41,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
} }
}; };
var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -53,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Attached_Value() public void Should_Track_Simple_Attached_Value()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "(Owner.Foo)"); var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -77,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
} }
}; };
var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () => Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{ {
var source = new Class1(); var source = new Class1();
var target = new ExpressionObserver(source, "(Owner.Foo)"); var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]);
return Tuple.Create(target, new WeakReference(source)); return Tuple.Create(target, new WeakReference(source));
}; };
@ -108,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Null(result.Item2.Target); Assert.Null(result.Item2.Target);
} }
[Fact]
public void Should_Fail_With_Attached_Property_With_Only_1_Part()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner)"));
}
[Fact]
public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner.Foo.Bar)"));
}
private static class Owner private static class Owner
{ {
public static readonly AttachedProperty<string> FooProperty = public static readonly AttachedProperty<string> FooProperty =

11
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Xunit; using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Base.UnitTests.Data.Core
{ {
@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value() public async Task Should_Get_Simple_Property_Value()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_ClrProperty_Value() public async Task Should_Get_Simple_ClrProperty_Value()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "ClrProperty"); var target = ExpressionObserver.Create(data, o => o.ClrProperty);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("clr-property", result); Assert.Equal("clr-property", result);
@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Property_Value() public void Should_Track_Simple_Property_Value()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -63,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () => Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{ {
var source = new Class1(); var source = new Class1();
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
return Tuple.Create(target, new WeakReference(source)); return Tuple.Create(target, new WeakReference(source));
}; };
@ -80,6 +81,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo"); AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); }
public string ClrProperty { get; } = "clr-property"; public string ClrProperty { get; } = "clr-property";
} }
} }

32
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@ -8,6 +8,7 @@ using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -19,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
{ {
var data = new ExceptionTest { MustBePositive = 5 }; var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
var validationMessageFound = false; var validationMessageFound = false;
observer.OfType<BindingNotification>() observer.OfType<BindingNotification>()
@ -36,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Exception_Validation_Sends_DataValidationError() public void Exception_Validation_Sends_DataValidationError()
{ {
var data = new ExceptionTest { MustBePositive = 5 }; var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var validationMessageFound = false; var validationMessageFound = false;
observer.OfType<BindingNotification>() observer.OfType<BindingNotification>()
@ -53,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
{ {
var data = new IndeiTest { MustBePositive = 5 }; var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
observer.Subscribe(_ => { }); observer.Subscribe(_ => { });
@ -64,7 +65,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Enabled_Indei_Validation_Subscribes() public void Enabled_Indei_Validation_Subscribes()
{ {
var data = new IndeiTest { MustBePositive = 5 }; var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var sub = observer.Subscribe(_ => { }); var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount); Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Validation_Plugins_Send_Correct_Notifications() public void Validation_Plugins_Send_Correct_Notifications()
{ {
var data = new IndeiTest(); var data = new IndeiTest();
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var result = new List<object>(); var result = new List<object>();
var errmsg = string.Empty; var errmsg = string.Empty;
@ -122,10 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Inner = new IndeiTest() Inner = new IndeiTest()
}; };
var observer = new ExpressionObserver( var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true);
data,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
observer.Subscribe(_ => { }); observer.Subscribe(_ => { });
@ -133,19 +131,16 @@ namespace Avalonia.Base.UnitTests.Data.Core
// intermediate object in a chain so for the moment I'm not sure what the result of // intermediate object in a chain so for the moment I'm not sure what the result of
// validating such a thing should look like. // validating such a thing should look like.
Assert.Equal(0, data.ErrorsChangedSubscriptionCount); Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount); Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount);
} }
[Fact] [Fact]
public void Sends_Correct_Notifications_With_Property_Chain() public void Sends_Correct_Notifications_With_Property_Chain()
{ {
var container = new Container(); var container = new Container();
var inner = new IndeiTest();
var observer = new ExpressionObserver( var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true);
container,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
var result = new List<object>(); var result = new List<object>();
observer.Subscribe(x => result.Add(x)); observer.Subscribe(x => result.Add(x));
@ -153,13 +148,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[] Assert.Equal(new[]
{ {
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
}, result); }, result);
GC.KeepAlive(container); GC.KeepAlive(container);
GC.KeepAlive(inner);
} }
public class ExceptionTest : NotifyingBase public class ExceptionTest : NotifyingBase
@ -220,9 +214,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
private class Container : IndeiBase private class Container : IndeiBase
{ {
private object _inner; private IndeiTest _inner;
public object Inner public IndeiTest Inner
{ {
get { return _inner; } get { return _inner; }
set { _inner = value; RaisePropertyChanged(); } set { _inner = value; RaisePropertyChanged(); }

224
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core
{
public class ExpressionObserverTests_ExpressionTree
{
[Fact]
public async Task IdentityExpression_Creates_IdentityObserver()
{
var target = new object();
var observer = ExpressionObserver.Create(target, o => o);
Assert.Equal(target, await observer.Take(1));
GC.KeepAlive(target);
}
[Fact]
public async Task Property_Access_Expression_Observes_Property()
{
var target = new Class1();
var observer = ExpressionObserver.Create(target, o => o.Foo);
Assert.Null(await observer.Take(1));
using (observer.Subscribe(_ => {}))
{
target.Foo = "Test";
}
Assert.Equal("Test", await observer.Take(1));
GC.KeepAlive(target);
}
[Fact]
public void Property_Acccess_Expression_Can_Set_Property()
{
var data = new Class1();
var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_List_Accessor_Can_Read_Value()
{
var data = new List<int> { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Complex_Index()
{
var data = new Dictionary<object, object>();
var key = new object();
data.Add(key, new object());
var target = ExpressionObserver.Create(data, o => o[key]);
Assert.Equal(data[key], await target.Take(1));
GC.KeepAlive(data);
}
[Fact]
public void Indexer_Can_Set_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.Create(data, o => o[0]);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(2));
}
GC.KeepAlive(data);
}
[Fact]
public async Task Inheritance_Casts_Should_Be_Ignored()
{
NotifyingBase test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo);
Assert.Equal("Test", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public void Convert_Casts_Should_Error()
{
var test = 1;
Assert.Throws<ExpressionParseException>(() => ExpressionObserver.Create(test, o => (double)o));
}
[Fact]
public async Task As_Operator_Should_Be_Ignored()
{
NotifyingBase test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => (o as Class1).Foo);
Assert.Equal("Test", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value()
{
var test = new Class2();
var target = ExpressionObserver.Create(test, o => o[Class2.FooProperty]);
Assert.Equal("foo", await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public async Task Complex_Expression_Correctly_Parsed()
{
var test = new Class1 { Foo = "Test" };
var target = ExpressionObserver.Create(test, o => o.Foo.Length);
Assert.Equal(test.Foo.Length, await target.Take(1));
GC.KeepAlive(test);
}
[Fact]
public void Should_Get_Completed_Task_Value()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = Task.FromResult("foo") };
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(new[] { "foo" }, result);
GC.KeepAlive(data);
}
}
[Fact]
public async Task Should_Create_Method_Binding()
{
var data = new Class3();
var target = ExpressionObserver.Create(data, o => (Action)o.Method);
var value = await target.Take(1);
Assert.IsAssignableFrom<Delegate>(value);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase
{
private string _foo;
public string Foo
{
get { return _foo; }
set
{
_foo = value;
RaisePropertyChanged(nameof(Foo));
}
}
}
private class Class2 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class2, string>("Foo", defaultValue: "foo");
public string ClrProperty { get; } = "clr-property";
}
private class Class3
{
public void Method() { }
}
}
}

89
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs

@ -11,6 +11,7 @@ using Avalonia.Diagnostics;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Base.UnitTests.Data.Core
{ {
@ -20,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Array_Value() public async Task Should_Get_Array_Value()
{ {
var data = new { Foo = new [] { "foo", "bar" } }; var data = new { Foo = new [] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, x => x.Foo[1]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -28,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = new ExpressionObserver(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact] [Fact]
public async Task Should_Get_MultiDimensional_Array_Value() public async Task Should_Get_MultiDimensional_Array_Value()
{ {
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[1, 1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("qux", result); Assert.Equal("qux", result);
@ -80,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Value_For_String_Indexer() public async Task Should_Get_Value_For_String_Indexer()
{ {
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } }; var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[foo]"); var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -92,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Value_For_Non_String_Indexer() public async Task Should_Get_Value_For_Non_String_Indexer()
{ {
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } }; var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = new ExpressionObserver(data, "Foo[1.0]"); var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -104,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]"); var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1,2]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result); Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -128,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]"); var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result); Assert.Equal(AvaloniaProperty.UnsetValue, result);
@ -140,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_List_Value() public async Task Should_Get_List_Value()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -152,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Add() public void Should_Track_INCC_Add()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]"); var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) using (var sub = target.Subscribe(x => result.Add(x)))
@ -170,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Remove() public void Should_Track_INCC_Remove()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[0]"); var target = ExpressionObserver.Create(data, o => o.Foo[0]);
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) using (var sub = target.Subscribe(x => result.Add(x)))
@ -188,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Replace() public void Should_Track_INCC_Replace()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) using (var sub = target.Subscribe(x => result.Add(x)))
@ -209,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
// method, but even if it did we need to test with ObservableCollection as well // method, but even if it did we need to test with ObservableCollection as well
// as AvaloniaList as it implements PropertyChanged as an explicit interface event. // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -225,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Reset() public void Should_Track_INCC_Reset()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -244,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo["foo"] = "bar"; data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux"; data.Foo["baz"] = "qux";
var target = new ExpressionObserver(data, "Foo[foo]"); var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) using (var sub = target.Subscribe(x => result.Add(x)))
@ -263,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_SetArrayIndex() public void Should_SetArrayIndex()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]"); var target = ExpressionObserver.Create(data, o => o.Foo[1]);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -285,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{"foo", 1 } {"foo", 1 }
} }
}; };
var target = new ExpressionObserver(data, "Foo[foo]"); var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
Assert.True(target.SetValue(4)); Assert.True(target.SetValue(4));
@ -307,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{"foo", 1 } {"foo", 1 }
} }
}; };
var target = new ExpressionObserver(data, "Foo[bar]"); var target = ExpressionObserver.Create(data, o => o.Foo["bar"]);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
Assert.True(target.SetValue(4)); Assert.True(target.SetValue(4));
@ -326,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Foo["foo"] = "bar"; data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux"; data.Foo["baz"] = "qux";
var target = new ExpressionObserver(data, "Foo[foo]"); var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -343,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new[] { 1, 2, 3 }; var data = new[] { 1, 2, 3 };
var target = new ExpressionObserver(data, "[1]"); var target = ExpressionObserver.Create(data, o => o[1]);
var value = await target.Take(1); var value = await target.Take(1);

17
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs

@ -9,6 +9,7 @@ using System.Reactive.Subjects;
using Microsoft.Reactive.Testing; using Microsoft.Reactive.Testing;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Xunit; using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Base.UnitTests.Data.Core
{ {
@ -18,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Source_Observable_Completes() public void Should_Complete_When_Source_Observable_Completes()
{ {
var source = new BehaviorSubject<object>(1); var source = new BehaviorSubject<object>(1);
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create<object, object>(source, o => o);
var completed = false; var completed = false;
target.Subscribe(_ => { }, () => completed = true); target.Subscribe(_ => { }, () => completed = true);
@ -31,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Source_Observable_Errors() public void Should_Complete_When_Source_Observable_Errors()
{ {
var source = new BehaviorSubject<object>(1); var source = new BehaviorSubject<object>(1);
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create<object, object>(source, o => o);
var completed = false; var completed = false;
target.Subscribe(_ => { }, () => completed = true); target.Subscribe(_ => { }, () => completed = true);
@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Update_Observable_Completes() public void Should_Complete_When_Update_Observable_Completes()
{ {
var update = new Subject<Unit>(); var update = new Subject<Unit>();
var target = new ExpressionObserver(() => 1, "Foo", update); var target = ExpressionObserver.Create(() => 1, o => o, update);
var completed = false; var completed = false;
target.Subscribe(_ => { }, () => completed = true); target.Subscribe(_ => { }, () => completed = true);
@ -57,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Complete_When_Update_Observable_Errors() public void Should_Complete_When_Update_Observable_Errors()
{ {
var update = new Subject<Unit>(); var update = new Subject<Unit>();
var target = new ExpressionObserver(() => 1, "Foo", update); var target = ExpressionObserver.Create(() => 1, o => o, update);
var completed = false; var completed = false;
target.Subscribe(_ => { }, () => completed = true); target.Subscribe(_ => { }, () => completed = true);
@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var scheduler = new TestScheduler(); var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable( var source = scheduler.CreateColdObservable(
OnNext(1, new { Foo = "foo" })); OnNext(1, new { Foo = "foo" }));
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
using (target.Subscribe(x => result.Add(x))) using (target.Subscribe(x => result.Add(x)))
@ -91,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var scheduler = new TestScheduler(); var scheduler = new TestScheduler();
var update = scheduler.CreateColdObservable<Unit>(); var update = scheduler.CreateColdObservable<Unit>();
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = new ExpressionObserver(() => data, "Foo", update); var target = ExpressionObserver.Create(() => data, o => o.Foo, update);
var result = new List<object>(); var result = new List<object>();
using (target.Subscribe(x => result.Add(x))) using (target.Subscribe(x => result.Add(x)))
@ -106,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
private Recorded<Notification<object>> OnNext(long time, object value) private Recorded<Notification<T>> OnNext<T>(long time, T value)
{ {
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value)); return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
} }
} }
} }

97
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Base.UnitTests.Data.Core
@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Negate_Boolean_Value() public async Task Should_Negate_Boolean_Value()
{ {
var data = new { Foo = true }; var data = new { Foo = true };
var target = new ExpressionObserver(data, "!Foo"); var target = ExpressionObserver.Create(data, o => !o.Foo);
var result = await target.Take(1); var result = await target.Take(1);
Assert.False((bool)result); Assert.False((bool)result);
@ -24,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
target.Subscribe(_ => { });
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
[Fact] [Fact]
public void Can_SetValue_For_Valid_Value() public void Can_SetValue_For_Valid_Value()
{ {
var data = new Test { Foo = true }; var data = new Test { Foo = true };
var target = new ExpressionObserver(data, "!Foo"); var target = ExpressionObserver.Create(data, o => !o.Foo);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
Assert.True(target.SetValue(true)); Assert.True(target.SetValue(true));

24
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
public class ExpressionObserverTests_Observable public class ExpressionObserverTests_Observable
{ {
[Fact] [Fact]
public void Should_Not_Get_Observable_Value_Without_Modifier_Char() public void Should_Not_Get_Observable_Value_Without_Streaming()
{ {
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var source = new BehaviorSubject<string>("foo"); var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source }; var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var source = new BehaviorSubject<string>("foo"); var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source }; var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^"); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Next^.Foo"); var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var source = new BehaviorSubject<string>("foo"); var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source }; var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^", true); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data1 = new Class1(); var data1 = new Class1();
var data2 = new Class2("foo"); var data2 = new Class2("foo");
var target = new ExpressionObserver(data1, "Next^.Foo", true); var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -127,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var data = new Class2("foo"); var data = new NotStreamable();
var target = new ExpressionObserver(data, "Foo^", true); var target = ExpressionObserver.Create(data, o => o.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -138,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[] new[]
{ {
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"), new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"),
BindingErrorType.Error) BindingErrorType.Error)
}, },
result); result);
@ -163,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
public string Foo { get; } public string Foo { get; }
} }
private class NotStreamable
{
public object StreamBinding() { throw new InvalidOperationException(); }
}
} }
} }

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

Loading…
Cancel
Save