Browse Source

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

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

+ 152 - 4
.editorconfig

@@ -1,11 +1,159 @@
-; This file is for unifying the coding style for different editors and IDEs.
-; More information at http://EditorConfig.org
+# editorconfig.org
 
+# top-most EditorConfig file
 root = true
 
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
 [*]
-end_of_line = CRLF
+insert_final_newline = true
+indent_style = space
+indent_size = 4
 
+# C# files
 [*.cs]
-indent_style = space
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+
+# avoid this. unless absolutely necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# prefer var
+csharp_style_var_for_built_in_types = true
+csharp_style_var_when_type_is_apparent = true
+csharp_style_var_elsewhere = true:suggestion
+
+# use language keywords instead of BCL types
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# name all constant fields using PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols  = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style    = pascal_case_style
+
+dotnet_naming_symbols.constant_fields.applicable_kinds   = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# static fields should have s_ prefix
+dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.static_fields_should_have_prefix.symbols  = static_fields
+dotnet_naming_rule.static_fields_should_have_prefix.style    = static_prefix_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds   = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+dotnet_naming_style.static_prefix_style.required_prefix = s_
+dotnet_naming_style.static_prefix_style.capitalization = camel_case 
+
+# internal and private fields should be _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols  = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style    = camel_case_underscore_style
+
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 
+
+# use accessibility modifiers
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# Code style defaults
+dotnet_sort_system_directives_first = true
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_operators = false:none
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+# Null checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Xaml files
+[*.xaml]
 indent_size = 4
+
+# Xml project files
+[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
+indent_size = 2
+
+# Xml build files
+[*.builds]
+indent_size = 2
+
+# Xml files
+[*.{xml,stylecop,resx,ruleset}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+# Shell scripts
+[*.sh]
+end_of_line = lf
+[*.{cmd, bat}]
+end_of_line = crlf

+ 6 - 2
.gitignore

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

+ 3 - 0
Avalonia.sln

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

+ 120 - 113
build.cake

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

+ 5 - 0
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>

+ 55 - 37
packages.cake

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

+ 5 - 7
parameters.cake

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

+ 2 - 0
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!
 
+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))
 
 Use these commands in Package Manager console to install Avalonia manually:

+ 2 - 2
samples/ControlCatalog/SideBar.xaml

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

+ 2 - 2
samples/RenderDemo/SideBar.xaml

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

+ 33 - 106
src/Avalonia.Base/AvaloniaObject.cs

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

+ 60 - 0
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;
+        }
+    }
+}

+ 1 - 6
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

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

+ 58 - 62
src/Avalonia.Base/Data/Core/ExpressionNode.cs

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

+ 0 - 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;
-        }
-    }
-}

+ 101 - 64
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

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

+ 2 - 2
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

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

+ 71 - 0
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 - 0
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);
+    }
+}

+ 1 - 1
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

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

+ 27 - 0
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 - 0
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}.");
+        }
+    }
+}

+ 6 - 34
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins
 
         private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
         {
-            if (!propertyName.Contains("."))
-            {
-                return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
-            }
-            else
-            {
-                var split = propertyName.Split('.');
-
-                if (split.Length == 2)
-                {
-                    // HACK: We need a way to resolve types here using something like IXamlTypeResolver.
-                    // We don't currently have that so we have to make our best guess.
-                    var type = split[0];
-                    var name = split[1];
-                    var registry = AvaloniaPropertyRegistry.Instance;
-                    var registered = registry.GetRegisteredAttached(o.GetType())
-                        .Concat(registry.GetRegistered(o.GetType()));
-
-                    foreach (var p in registered)
-                    {
-                        if (p.Name == name && IsOfType(p.OwnerType, type))
-                        {
-                            return p;
-                        }
-                    }
-                }
-            }
-
-            return null;
+            return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
         }
 
         private static bool IsOfType(Type type, string typeName)
@@ -145,15 +117,15 @@ namespace Avalonia.Data.Core.Plugins
                 return false;
             }
 
-            protected override void Dispose(bool disposing)
+            protected override void SubscribeCore()
             {
-                _subscription?.Dispose();
-                _subscription = null;
+                _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
             }
 
-            protected override void SubscribeCore(IObserver<object> observer)
+            protected override void UnsubscribeCore()
             {
-                _subscription = Instance?.GetObservable(_property).Subscribe(observer);
+                _subscription?.Dispose();
+                _subscription = null;
             }
         }
     }

+ 5 - 5
src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs

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

+ 2 - 3
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

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

+ 12 - 2
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs

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

+ 9 - 10
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

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

+ 8 - 8
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

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

+ 6 - 2
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

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

+ 38 - 30
src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs

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

+ 6 - 5
src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs

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

+ 16 - 15
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

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

+ 1 - 1
src/Avalonia.Base/Data/Core/SettableNode.cs

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

+ 27 - 0
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");
+        }
+    }
+}

+ 13 - 6
src/Avalonia.Base/Data/Core/StreamNode.cs

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

+ 5 - 4
src/Avalonia.Base/IPriorityValueOwner.cs

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

+ 2 - 2
src/Avalonia.Base/PriorityValue.cs

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

+ 172 - 0
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;
+        }
+    }
+}

+ 27 - 18
src/Avalonia.Controls/AppBuilderBase.cs

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

+ 105 - 7
src/Avalonia.Controls/Application.cs

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

+ 1 - 1
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

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

+ 1 - 1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs

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

+ 26 - 0
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 - 0
src/Avalonia.Controls/ItemsControl.cs

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

+ 1 - 1
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -518,7 +518,7 @@ namespace Avalonia.Controls.Presenters
                 }
 
                 var container = generator.ContainerFromIndex(index);
-                var layoutManager = LayoutManager.Instance;
+                var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager;
 
                 // We need to do a layout here because it's possible that the container we moved to
                 // is only partially visible due to differing item sizes. If the container is only 

+ 0 - 1
src/Avalonia.Controls/Primitives/AdornerLayer.cs

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

+ 7 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

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

+ 1 - 0
src/Avalonia.Controls/Primitives/TemplatedControl.cs

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

+ 11 - 8
src/Avalonia.Controls/StackPanel.cs

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

+ 1 - 1
src/Avalonia.Controls/TextBox.cs

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

+ 17 - 1
src/Avalonia.Controls/TopLevel.cs

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

+ 39 - 28
src/Avalonia.Controls/Window.cs

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

+ 2 - 3
src/Avalonia.Controls/WindowBase.cs

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

+ 134 - 0
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 - 0
src/Avalonia.Layout/ILayoutRoot.cs

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

+ 1 - 6
src/Avalonia.Layout/LayoutManager.cs

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

+ 3 - 7
src/Avalonia.Layout/Layoutable.cs

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

+ 1 - 1
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

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

+ 1 - 1
src/Avalonia.Themes.Default/DropDown.xaml

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

+ 2 - 7
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -45,7 +45,7 @@
             <Popup Name="PART_Popup"
                    PlacementMode="Right"
                    StaysOpen="True"
-                   IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
+                   IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
                    ObeyScreenEdges="True">
               <Border Background="{TemplateBinding Background}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
@@ -92,7 +92,7 @@
               </ContentPresenter.DataTemplates>
             </ContentPresenter>
             <Popup Name="PART_Popup"
-                   IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
+                   IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
                    StaysOpen="True" 
                    ObeyScreenEdges="True">
               <Border Background="{TemplateBinding Background}"
@@ -122,11 +122,6 @@
     </Setter>
   </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">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

+ 2 - 2
src/Avalonia.Themes.Default/ScrollBar.xaml

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

+ 5 - 5
src/Avalonia.Themes.Default/ScrollViewer.xaml

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

+ 3 - 4
src/Avalonia.Themes.Default/Slider.xaml

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

+ 2 - 2
src/Avalonia.Themes.Default/TabControl.xaml

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

+ 1 - 1
src/Avalonia.Themes.Default/TextBox.xaml

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

+ 1 - 3
src/Avalonia.Themes.Default/TreeViewItem.xaml

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

+ 1 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

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

+ 277 - 393
src/Avalonia.Visuals/Media/PathMarkupParser.cs

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

+ 0 - 1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

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

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

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

+ 40 - 45
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

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

+ 1 - 1
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)
         {
-            var parser = new SelectorParser((t, ns) => context.ResolveType(ns, t));
+            var parser = new SelectorParser(context.ResolveType);
 
             return parser.Parse((string)value);
         }

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

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

+ 0 - 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;
-    }
-}

+ 5 - 3
src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs

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

+ 4 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs

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

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

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

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

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

+ 23 - 12
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -8,6 +8,7 @@ using System.Reactive.Linq;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Core;
 using Avalonia.LogicalTree;
+using Avalonia.Markup.Parsers;
 using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
@@ -85,6 +86,11 @@ namespace Avalonia.Data
 
         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/>
         public InstancedBinding Initiate(
             IAvaloniaObject target,
@@ -189,20 +195,22 @@ namespace Avalonia.Data
 
             if (!targetIsDataContext)
             {
-                var result = new ExpressionObserver(
+                var result = ExpressionObserverBuilder.Build(
                     () => target.GetValue(StyledElement.DataContextProperty),
                     path,
                     new UpdateSignal(target, StyledElement.DataContextProperty),
-                    enableDataValidation);
+                    enableDataValidation,
+                    typeResolver: TypeResolver);
 
                 return result;
             }
             else
             {
-                return new ExpressionObserver(
+                return ExpressionObserverBuilder.Build(
                     GetParentDataContext(target),
                     path,
-                    enableDataValidation);
+                    enableDataValidation,
+                    typeResolver: TypeResolver);
             }
         }
 
@@ -215,11 +223,12 @@ namespace Avalonia.Data
             Contract.Requires<ArgumentNullException>(target != null);
 
             var description = $"#{elementName}.{path}";
-            var result = new ExpressionObserver(
+            var result = ExpressionObserverBuilder.Build(
                 ControlLocator.Track(target, elementName),
                 path,
                 enableDataValidation,
-                description);
+                description,
+                typeResolver: TypeResolver);
             return result;
         }
 
@@ -251,10 +260,11 @@ namespace Avalonia.Data
                     throw new InvalidOperationException("Invalid tree to traverse.");
             }
 
-            return new ExpressionObserver(
+            return ExpressionObserverBuilder.Build(
                 controlLocator,
                 path,
-                enableDataValidation);
+                enableDataValidation,
+                typeResolver: TypeResolver);
         }
 
         private ExpressionObserver CreateSourceObserver(
@@ -264,7 +274,7 @@ namespace Avalonia.Data
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            return new ExpressionObserver(source, path, enableDataValidation);
+            return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
         }
 
         private ExpressionObserver CreateTemplatedParentObserver(
@@ -273,12 +283,13 @@ namespace Avalonia.Data
             bool enableDataValidation)
         {
             Contract.Requires<ArgumentNullException>(target != null);
-
-            var result = new ExpressionObserver(
+            
+            var result = ExpressionObserverBuilder.Build(
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
                 path,
                 new UpdateSignal(target, StyledElement.TemplatedParentProperty),
-                enableDataValidation);
+                enableDataValidation,
+                typeResolver: TypeResolver);
 
             return result;
         }

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

+ 2 - 1
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.
 // 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.Collections.Generic;
 using System.Text;
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
     internal static class ArgumentListParser
     {

+ 75 - 0
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);
+        }
+    }
+}

+ 29 - 6
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.
 // 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.Collections.Generic;
 using System.Linq;
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
     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;
         }
 
@@ -130,7 +134,19 @@ namespace Avalonia.Data.Core.Parsers
 
         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('.'))
             {
@@ -144,7 +160,14 @@ namespace Avalonia.Data.Core.Parsers
                 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;
         }
 
@@ -157,7 +180,7 @@ namespace Avalonia.Data.Core.Parsers
                 throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
             }
 
-            nodes.Add(new IndexerNode(args));
+            nodes.Add(new StringIndexerNode(args));
             return State.AfterMember;
         }
         

+ 1 - 1
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.Text;
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
     internal static class IdentifierParser
     {

+ 14 - 67
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.Reactive.Linq;
 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;
         }
 
         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)
         {
             var typeInfo = Target.Target.GetType().GetTypeInfo();
@@ -156,7 +129,7 @@ namespace Avalonia.Data.Core
 
         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 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)
-            {
-                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
+            var typeInfo = sender.GetType().GetTypeInfo();
+            return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
         }
 
-        private bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
+        protected override int? TryGetFirstArgumentAsInt()
         {
-            var typeInfo = sender.GetType().GetTypeInfo();
-            return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
+            if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
+            {
+                return (int?)value;
+            }
+            return null;
         }
     }
 }

+ 1 - 1
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs

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

+ 2 - 2
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

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

+ 1 - 1
src/OSX/Avalonia.MonoMac/KeyTransform.cs

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

+ 9 - 1
src/OSX/Avalonia.MonoMac/WindowImpl.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Platform;
 using MonoMac.AppKit;
 using MonoMac.CoreGraphics;
+using Avalonia.Threading;
 
 namespace Avalonia.MonoMac
 {
@@ -16,7 +17,14 @@ namespace Avalonia.MonoMac
 
         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.DidResize += delegate

+ 1 - 1
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

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

+ 3 - 0
src/Windows/Avalonia.Win32/ClipboardImpl.cs

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

+ 22 - 21
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

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

+ 5 - 25
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs

@@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
 {
     public class ExpressionObserverTests_AttachedProperty
     {
-        public ExpressionObserverTests_AttachedProperty()
-        {
-            var foo = Owner.FooProperty;
-        }
 
         [Fact]
         public async Task Should_Get_Attached_Property_Value()
         {
             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);
 
             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);
 
             Assert.Equal("bar", result);
@@ -53,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Simple_Attached_Value()
         {
             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 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 sub = target.Subscribe(x => result.Add(x));
@@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
                 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));
             };
 
@@ -108,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
             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
         {
             public static readonly AttachedProperty<string> FooProperty =

+ 7 - 4
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs

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

+ 13 - 19
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

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

+ 224 - 0
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() { }
+        }
+    }
+}

+ 21 - 68
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs

@@ -11,6 +11,7 @@ using Avalonia.Diagnostics;
 using Avalonia.Data.Core;
 using Avalonia.UnitTests;
 using Xunit;
+using Avalonia.Markup.Parsers;
 
 namespace Avalonia.Base.UnitTests.Data.Core
 {
@@ -20,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Array_Value()
         {
             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);
 
             Assert.Equal("bar", result);
@@ -28,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             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]
         public async Task Should_Get_MultiDimensional_Array_Value()
         {
             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);
 
             Assert.Equal("qux", result);
@@ -80,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Value_For_String_Indexer()
         {
             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);
 
             Assert.Equal("bar", result);
@@ -92,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         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 target = new ExpressionObserver(data, "Foo[1.0]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -104,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
         {
             var data = new { Foo = new[] { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "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 target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = await target.Take(1);
 
             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()
         {
             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);
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -140,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_List_Value()
         {
             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);
 
             Assert.Equal("bar", result);
@@ -152,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Add()
         {
             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>();
 
             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()
         {
             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>();
 
             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()
         {
             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>();
 
             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
             // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
             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 sub = target.Subscribe(x => result.Add(x));
@@ -225,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Reset()
         {
             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 sub = target.Subscribe(x => result.Add(x));
@@ -244,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             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>();
 
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -263,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_SetArrayIndex()
         {
             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(_ => { }))
             {
@@ -285,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                 }
             };
-            
-            var target = new ExpressionObserver(data, "Foo[foo]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             using (target.Subscribe(_ => { }))
             {
                 Assert.True(target.SetValue(4));
@@ -307,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                 }
             };
-            
-            var target = new ExpressionObserver(data, "Foo[bar]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["bar"]);
             using (target.Subscribe(_ => { }))
             {
                 Assert.True(target.SetValue(4));
@@ -326,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             data.Foo["baz"] = "qux";
 
-            var target = new ExpressionObserver(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
 
             using (target.Subscribe(_ => { }))
             {
@@ -343,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             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);
 

+ 9 - 8
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs

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

+ 3 - 94
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs

@@ -6,6 +6,7 @@ using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests.Data.Core
@@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Negate_Boolean_Value()
         {
             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);
 
             Assert.False((bool)result);
@@ -24,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             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]
         public void Can_SetValue_For_Valid_Value()
         {
             var data = new Test { Foo = true };
-            var target = new ExpressionObserver(data, "!Foo");
+            var target = ExpressionObserver.Create(data, o => !o.Foo);
             target.Subscribe(_ => { });
 
             Assert.True(target.SetValue(true));

+ 15 - 9
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@@ -7,6 +7,7 @@ using System.Reactive.Linq;
 using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
     public class ExpressionObserverTests_Observable
     {
         [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())
             {
                 var source = new BehaviorSubject<string>("foo");
                 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 sub = target.Subscribe(x => result.Add(x));
@@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var source = new BehaviorSubject<string>("foo");
                 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 sub = target.Subscribe(x => result.Add(x));
@@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 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 sub = target.Subscribe(x => result.Add(x));
@@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var source = new BehaviorSubject<string>("foo");
                 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 sub = target.Subscribe(x => result.Add(x));
@@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var data1 = new Class1();
                 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 sub = target.Subscribe(x => result.Add(x));
@@ -127,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
-                var data = new Class2("foo");
-                var target = new ExpressionObserver(data, "Foo^", true);
+                var data = new NotStreamable();
+                var target = ExpressionObserver.Create(data, o => o.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -138,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     new[]
                     {
                         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)
                     },
                     result);
@@ -163,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
             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