Browse Source

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

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

156
.editorconfig

@ -1,11 +1,159 @@
; This file is for unifying the coding style for different editors and IDEs.
; 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

8
.gitignore

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

3
Avalonia.sln

@ -58,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9B9E
EndProject
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

233
build.cake

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

5
build/System.Memory.props

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

92
packages.cake

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

12
parameters.cake

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

2
readme.md

@ -20,6 +20,8 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl
Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
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:

4
samples/ControlCatalog/SideBar.xaml

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

4
samples/RenderDemo/SideBar.xaml

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

139
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
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
public class AvaloniaPropertyAccessorNode : SettableNode
{
private IDisposable _subscription;
private readonly bool _enableValidation;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation)
{
_property = property;
_enableValidation = enableValidation;
}
public override string Description => PropertyName;
public string PropertyName { get; }
public override Type PropertyType => _property.PropertyType;
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
try
{
if (Target.IsAlive && Target.Target is IAvaloniaObject obj)
{
obj.SetValue(_property, value, priority);
return true;
}
return false;
}
catch
{
return false;
}
}
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is IAvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

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

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

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

@ -2,22 +2,18 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Data.Core
{
internal abstract class ExpressionNode : ISubject<object>
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(

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

@ -1,30 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data.Core.Parsers;
namespace Avalonia.Data.Core
{
internal static class ExpressionNodeBuilder
{
public static ExpressionNode Build(string expression, bool enableValidation = false)
{
if (string.IsNullOrWhiteSpace(expression))
{
throw new ArgumentException("'expression' may not be empty.");
}
var reader = new Reader(expression);
var parser = new ExpressionParser(enableValidation);
var node = parser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader.Position, "Expected end of expression.");
}
return node;
}
}
}

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

@ -3,9 +3,11 @@
using System;
using System.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);
}
}
}

4
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
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Data.Core
{
class IndexerExpressionNode : IndexerNodeBase
{
private readonly ParameterExpression _parameter;
private readonly IndexExpression _expression;
private readonly Delegate _setDelegate;
private readonly Delegate _getDelegate;
private readonly Delegate _firstArgumentDelegate;
public IndexerExpressionNode(IndexExpression expression)
{
_parameter = Expression.Parameter(expression.Object.Type);
_expression = expression.Update(_parameter, expression.Arguments);
_getDelegate = Expression.Lambda(_expression, _parameter).Compile();
var valueParameter = Expression.Parameter(expression.Type);
_setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile();
_firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile();
}
public override Type PropertyType => _expression.Type;
public override string Description => _expression.ToString();
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
try
{
_setDelegate.DynamicInvoke(Target.Target, value);
return true;
}
catch (Exception)
{
return false;
}
}
protected override object GetValue(object target)
{
try
{
return _getDelegate.DynamicInvoke(target);
}
catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException
|| e.InnerException is IndexOutOfRangeException
|| e.InnerException is KeyNotFoundException)
{
return AvaloniaProperty.UnsetValue;
}
}
protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{
return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
}
}

92
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -0,0 +1,92 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
public abstract class IndexerNodeBase : SettableNode
{
private IDisposable _subscription;
protected override void StartListeningCore(WeakReference reference)
{
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
}
protected abstract object GetValue(object target);
protected abstract int? TryGetFirstArgumentAsInt();
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is IList)
{
var index = TryGetFirstArgumentAsInt();
if (index == null)
{
return false;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
return index >= e.NewStartingIndex;
case NotifyCollectionChangedAction.Remove:
return index >= e.OldStartingIndex;
case NotifyCollectionChangedAction.Replace:
return index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count;
case NotifyCollectionChangedAction.Move:
return (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems.Count);
case NotifyCollectionChangedAction.Reset:
return true;
}
}
return true; // Implementation defined meaning for the index, so just try to update anyway
}
protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e);
}
}

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

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

27
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Avalonia.Data.Core.Parsers
{
static class ExpressionTreeParser
{
public static ExpressionNode Parse(Expression expr, bool enableDataValidation)
{
var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation);
visitor.Visit(expr);
var nodes = visitor.Nodes;
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault() ?? new EmptyExpressionNode();
}
}
}

219
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Avalonia.Data.Core.Parsers
{
class ExpressionVisitorNodeBuilder : ExpressionVisitor
{
private const string MultiDimensionalArrayGetterMethodName = "Get";
private static PropertyInfo AvaloniaObjectIndexer;
private static MethodInfo CreateDelegateMethod;
private readonly bool _enableDataValidation;
static ExpressionVisitorNodeBuilder()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) });
CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) });
}
public List<ExpressionNode> Nodes { get; }
public ExpressionVisitorNodeBuilder(bool enableDataValidation)
{
_enableDataValidation = enableDataValidation;
Nodes = new List<ExpressionNode>();
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
Nodes.Add(new LogicalNotNode());
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand.Type.IsAssignableFrom(node.Type))
{
// Ignore inheritance casts
}
else
{
throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression.");
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
// Ignore as operator.
}
else
{
throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression");
}
return base.VisitUnary(node);
}
protected override Expression VisitMember(MemberExpression node)
{
var visited = base.VisitMember(node);
Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation));
return visited;
}
protected override Expression VisitIndex(IndexExpression node)
{
Visit(node.Object);
if (node.Indexer == AvaloniaObjectIndexer)
{
var property = GetArgumentExpressionValue<AvaloniaProperty>(node.Arguments[0]);
Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation));
}
else
{
Nodes.Add(new IndexerExpressionNode(node));
}
return node;
}
private T GetArgumentExpressionValue<T>(Expression expr)
{
try
{
return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
}
catch (InvalidOperationException ex)
{
throw new ExpressionParseException(0, "Unable to parse indexer value.", ex);
}
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.ArrayIndex)
{
return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitBlock(BlockExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLabel(LabelExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitListInit(ListInitExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLoop(LoopExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method == CreateDelegateMethod)
{
var visited = Visit(node.Arguments[1]);
Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue<MethodInfo>(node.Object).Name, _enableDataValidation));
return node;
}
else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
{
if (node.Method.IsStatic)
{
Visit(node.Arguments[0]);
}
else
{
Visit(node.Object);
}
Nodes.Add(new StreamNode());
return node;
}
var property = TryGetPropertyFromMethod(node.Method);
if (property != null)
{
return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
}
else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName)
{
return Visit(Expression.MakeIndex(node.Object, null, node.Arguments));
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'.");
}
private PropertyInfo TryGetPropertyFromMethod(MethodInfo method)
{
var type = method.DeclaringType;
return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
protected override Expression VisitSwitch(SwitchExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTry(TryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
}
}

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

@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins
private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
{
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;
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
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
src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia
{
public static class StreamBindingExtensions
{
internal static string StreamBindingName = "StreamBinding";
public static T StreamBinding<T>(this Task<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static object StreamBinding(this Task @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static T StreamBinding<T>(this IObservable<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
}
}

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

@ -2,30 +2,37 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
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;
}
}
}

9
src/Avalonia.Base/IPriorityValueOwner.cs

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

4
src/Avalonia.Base/PriorityValue.cs

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

172
src/Avalonia.Base/ValueStore.cs

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

45
src/Avalonia.Controls/AppBuilderBase.cs

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

112
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)

2
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()

2
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
src/Avalonia.Controls/ExitMode.cs

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

1
src/Avalonia.Controls/ItemsControl.cs

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

2
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

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

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

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

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

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

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

19
src/Avalonia.Controls/StackPanel.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
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);

2
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)
{

18
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);
}

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

5
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
src/Avalonia.Controls/WindowCollection.cs

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

5
src/Avalonia.Layout/ILayoutRoot.cs

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

7
src/Avalonia.Layout/LayoutManager.cs

@ -19,11 +19,6 @@ namespace Avalonia.Layout
private bool _queued;
private bool _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);
}

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

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

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

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

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

9
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}"/>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3
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);

3
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);
}

35
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
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

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

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

@ -1,11 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// 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
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

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

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

@ -1,18 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// 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;
}

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

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

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

@ -12,46 +12,19 @@ using System.Linq;
using System.Reflection;
using System.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;
}
}
}

2
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
{

4
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)
{

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

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

10
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

2
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
src/Windows/Avalonia.Win32/ClipboardImpl.cs

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

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

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.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();

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

@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
public class ExpressionObserverTests_AttachedProperty
{
public 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 =

11
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";
}
}

32
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
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs

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

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

@ -11,6 +11,7 @@ using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Avalonia.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);

17
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));
}
}
}

97
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));

24
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

Loading…
Cancel
Save