Browse Source

Merge branch 'master' into feature/stringformat

pull/1684/head
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
8874143a82
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 156
      .editorconfig
  2. 13
      .gitignore
  3. 5
      .ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject
  4. 5
      .ncrunch/BindingDemo.net461.v3.ncrunchproject
  5. 5
      .ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject
  6. 5
      .ncrunch/Previewer.v3.ncrunchproject
  7. 5
      .ncrunch/RemoteDemo.v3.ncrunchproject
  8. 5
      .ncrunch/RenderDemo.net461.v3.ncrunchproject
  9. 5
      .ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject
  10. 5
      .ncrunch/VirtualizationDemo.net461.v3.ncrunchproject
  11. 5
      .ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject
  12. 3
      Avalonia.sln
  13. 233
      build.cake
  14. 5
      build/System.Memory.props
  15. 92
      packages.cake
  16. 12
      parameters.cake
  17. 2
      readme.md
  18. 26
      samples/BindingDemo/MainWindow.xaml
  19. 6
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  20. 6
      samples/ControlCatalog/Pages/BorderPage.xaml
  21. 10
      samples/ControlCatalog/Pages/ButtonPage.xaml
  22. 6
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
  23. 6
      samples/ControlCatalog/Pages/CalendarPage.xaml
  24. 4
      samples/ControlCatalog/Pages/CanvasPage.xaml
  25. 10
      samples/ControlCatalog/Pages/CarouselPage.xaml
  26. 10
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  27. 6
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  28. 6
      samples/ControlCatalog/Pages/DatePickerPage.xaml
  29. 4
      samples/ControlCatalog/Pages/DialogsPage.xaml
  30. 6
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  31. 6
      samples/ControlCatalog/Pages/DropDownPage.xaml
  32. 6
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  33. 4
      samples/ControlCatalog/Pages/ImagePage.xaml
  34. 4
      samples/ControlCatalog/Pages/MenuPage.xaml
  35. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  36. 6
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  37. 8
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  38. 4
      samples/ControlCatalog/Pages/SliderPage.xaml
  39. 11
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  40. 2
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  41. 4
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  42. 4
      samples/ControlCatalog/SideBar.xaml
  43. 4
      samples/RenderDemo/SideBar.xaml
  44. 2
      samples/VirtualizationDemo/MainWindow.xaml
  45. 126
      src/Avalonia.Animation/Animation.cs
  46. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  47. 191
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  48. 80
      src/Avalonia.Animation/Animator`1.cs
  49. 2
      src/Avalonia.Animation/Cue.cs
  50. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  51. 10
      src/Avalonia.Animation/IAnimation.cs
  52. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  53. 2
      src/Avalonia.Animation/IAnimator.cs
  54. 16
      src/Avalonia.Animation/KeyFrame.cs
  55. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  56. 217
      src/Avalonia.Base/AvaloniaObject.cs
  57. 81
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  58. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  59. 59
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  60. 60
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  61. 45
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  62. 7
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  63. 120
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  64. 30
      src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs
  65. 212
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  66. 4
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  67. 71
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  68. 92
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  69. 2
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  70. 199
      src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs
  71. 27
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  72. 219
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  73. 40
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  74. 10
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  75. 5
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  76. 14
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  77. 19
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  78. 16
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  79. 8
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  80. 68
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  81. 11
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  82. 31
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  83. 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  84. 27
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  85. 19
      src/Avalonia.Base/Data/Core/StreamNode.cs
  86. 25
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  87. 12
      src/Avalonia.Base/IPriorityValueOwner.cs
  88. 15
      src/Avalonia.Base/PriorityValue.cs
  89. 42
      src/Avalonia.Base/Reactive/AvaloniaObservable.cs
  90. 46
      src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs
  91. 52
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  92. 202
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  93. 76
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  94. 85
      src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs
  95. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  96. 53
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  97. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  98. 58
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  99. 182
      src/Avalonia.Base/ValueStore.cs
  100. 45
      src/Avalonia.Controls/AppBuilderBase.cs

156
.editorconfig

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

13
.gitignore

@ -165,6 +165,11 @@ $RECYCLE.BIN/
################# #################
.idea .idea
#################
## VS Code
#################
.vscode/
################# #################
## Cake ## Cake
################# #################
@ -176,5 +181,9 @@ nuget
Avalonia.XBuild.sln Avalonia.XBuild.sln
project.lock.json project.lock.json
.idea/* .idea/*
**/obj-Skia/*
**/obj-Direct2D1/*
##################
## BenchmarkDotNet
##################
BenchmarkDotNet.Artifacts/

5
.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/BindingDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Previewer.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RemoteDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RenderDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

3
Avalonia.sln

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

233
build.cake

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

5
build/System.Memory.props

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

92
packages.cake

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

12
parameters.cake

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

2
readme.md

@ -20,6 +20,8 @@ Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a fl
Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms! Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed))
Use these commands in Package Manager console to install Avalonia manually: Use these commands in Package Manager console to install Avalonia manually:

26
samples/BindingDemo/MainWindow.xaml

@ -18,18 +18,18 @@
<TabItem Header="Basic"> <TabItem Header="Basic">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/> <TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/> <TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/> <TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/> <TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/> <TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/> <TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button> <Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/> <TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/> <TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox> <CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
@ -37,13 +37,13 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left"> <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/> <TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/> <TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding Path=DoubleValue}"/> <TextBlock Text="{Binding Path=DoubleValue}"/>
<ProgressBar Maximum="10" Value="{Binding DoubleValue}"/> <ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left"> <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Binding Sources"/> <TextBlock FontSize="16" Text="Binding Sources"/>
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding #first.Text, Mode=TwoWay}"/> Text="{Binding #first.Text, Mode=TwoWay}"/>
@ -52,7 +52,7 @@
<TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True" <TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/> Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left"> <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Scheduler"/> <TextBlock FontSize="16" Text="Scheduler"/>
<TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/> <TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
<TextBlock FontSize="16" Text="Stream Operator"/> <TextBlock FontSize="16" Text="Stream Operator"/>
@ -68,11 +68,11 @@
<TextBlock Text="{Binding StringValue}"/> <TextBlock Text="{Binding StringValue}"/>
</DataTemplate> </DataTemplate>
</StackPanel.DataTemplates> </StackPanel.DataTemplates>
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/> <TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/> <ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/> <TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/> <ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
</StackPanel> </StackPanel>
@ -87,16 +87,16 @@
</TabItem> </TabItem>
<TabItem Header="Property Validation"> <TabItem Header="Property Validation">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}"> <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<TextBlock FontSize="16" Text="Exception Validation"/> <TextBlock FontSize="16" Text="Exception Validation"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/> <TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}"> <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
<TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/> <TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
<TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/> <TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
<TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/> <TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}"> <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<TextBlock FontSize="16" Text="Data Annotations Validation"/> <TextBlock FontSize="16" Text="Data Annotations Validation"/>
<TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/> <TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/> <TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
@ -104,7 +104,7 @@
</StackPanel> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="Commands"> <TabItem Header="Commands">
<StackPanel Margin="18" Gap="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/> <Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/>
<ToggleButton Content="ToggleButton" IsChecked="{Binding BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="ToggleButton"/> <ToggleButton Content="ToggleButton" IsChecked="{Binding BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="ToggleButton"/>
<CheckBox Content="CheckBox" IsChecked="{Binding !BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="CheckBox"/> <CheckBox Content="CheckBox" IsChecked="{Binding !BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="CheckBox"/>
@ -114,4 +114,4 @@
</StackPanel> </StackPanel>
</TabItem> </TabItem>
</TabControl> </TabControl>
</Window> </Window>

6
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">AutoCompleteBox</TextBlock> <TextBlock Classes="h1">AutoCompleteBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock> <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="8"> Spacing="8">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock Text="MinimumPrefixLength: 1"/> <TextBlock Text="MinimumPrefixLength: 1"/>
<AutoCompleteBox Width="200" <AutoCompleteBox Width="200"
@ -56,4 +56,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/BorderPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Border</TextBlock> <TextBlock Classes="h1">Border</TextBlock>
<TextBlock Classes="h2">A control which decorates a child with a border and background</TextBlock> <TextBlock Classes="h2">A control which decorates a child with a border and background</TextBlock>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16"> <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16">
<TextBlock>Border</TextBlock> <TextBlock>Border</TextBlock>
</Border> </Border>
@ -29,4 +29,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

10
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -1,14 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Button</TextBlock> <TextBlock Classes="h1">Button</TextBlock>
<TextBlock Classes="h2">A button control</TextBlock> <TextBlock Classes="h2">A button control</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" Gap="8" Width="150"> <StackPanel Orientation="Vertical" Spacing="8" Width="150">
<Button>Button</Button> <Button>Button</Button>
<Button Foreground="White">Foreground</Button> <Button Foreground="White">Foreground</Button>
<Button Background="{DynamicResource ThemeAccentBrush}">Background</Button> <Button Background="{DynamicResource ThemeAccentBrush}">Background</Button>
@ -25,7 +25,7 @@
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" Gap="8" Width="150"> <StackPanel Orientation="Vertical" Spacing="8" Width="150">
<Button BorderThickness="0">No Border</Button> <Button BorderThickness="0">No Border</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}">Border Color</Button> <Button BorderBrush="{DynamicResource ThemeAccentBrush}">Border Color</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4">Thick Border</Button> <Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4">Thick Border</Button>
@ -33,4 +33,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@ -1,11 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ButtonSpinner</TextBlock> <TextBlock Classes="h1">ButtonSpinner</TextBlock>
<TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock> <TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
<StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0"> <StackPanel Orientation="Vertical" Spacing="8" Width="200" Margin="0,20,0,0">
<CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox> <CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
<CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox> <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
<ButtonSpinner Spin="OnSpin" Height="30" <ButtonSpinner Spin="OnSpin" Height="30"
@ -21,4 +21,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/CalendarPage.xaml

@ -1,13 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Calendar</TextBlock> <TextBlock Classes="h1">Calendar</TextBlock>
<TextBlock Classes="h2">A calendar control for selecting dates</TextBlock> <TextBlock Classes="h2">A calendar control for selecting dates</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock Text="SelectionMode: None"/> <TextBlock Text="SelectionMode: None"/>
<Calendar SelectionMode="None" <Calendar SelectionMode="None"
@ -44,4 +44,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

4
samples/ControlCatalog/Pages/CanvasPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Canvas</TextBlock> <TextBlock Classes="h1">Canvas</TextBlock>
<TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock> <TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock>
<Canvas Background="Yellow" Width="300" Height="400"> <Canvas Background="Yellow" Width="300" Height="400">
@ -31,4 +31,4 @@
<Polyline Points="0,0 65,0 78,-26 91,39 104,-39 117,13 130,0 195,0" Stroke="Brown" Canvas.Left="30" Canvas.Top="350"/> <Polyline Points="0,0 65,0 78,-26 91,39 104,-39 117,13 130,0 195,0" Stroke="Brown" Canvas.Left="30" Canvas.Top="350"/>
</Canvas> </Canvas>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

10
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Carousel</TextBlock> <TextBlock Classes="h1">Carousel</TextBlock>
<TextBlock Classes="h2">An items control that displays its items as pages that fill the control.</TextBlock> <TextBlock Classes="h2">An items control that displays its items as pages that fill the control.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<Button Name="left" VerticalAlignment="Center" Padding="20"> <Button Name="left" VerticalAlignment="Center" Padding="20">
<Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/> <Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
</Button> </Button>
@ -20,7 +20,7 @@
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Gap="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Transition</TextBlock> <TextBlock VerticalAlignment="Center">Transition</TextBlock>
<DropDown Name="transition" SelectedIndex="1" VerticalAlignment="Center"> <DropDown Name="transition" SelectedIndex="1" VerticalAlignment="Center">
<DropDownItem>None</DropDownItem> <DropDownItem>None</DropDownItem>
@ -29,7 +29,7 @@
</DropDown> </DropDown>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Gap="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock> <TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<DropDown Name="orientation" SelectedIndex="1" VerticalAlignment="Center"> <DropDown Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
<DropDownItem>Horizontal</DropDownItem> <DropDownItem>Horizontal</DropDownItem>
@ -38,4 +38,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

10
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@ -1,15 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">CheckBox</TextBlock> <TextBlock Classes="h1">CheckBox</TextBlock>
<TextBlock Classes="h2">A check box control</TextBlock> <TextBlock Classes="h2">A check box control</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Gap="16"> Spacing="16">
<CheckBox>Unchecked</CheckBox> <CheckBox>Unchecked</CheckBox>
<CheckBox IsChecked="True">Checked</CheckBox> <CheckBox IsChecked="True">Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox> <CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
@ -17,7 +17,7 @@
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<CheckBox IsChecked="False" IsThreeState="True">Three State: Unchecked</CheckBox> <CheckBox IsChecked="False" IsThreeState="True">Three State: Unchecked</CheckBox>
<CheckBox IsChecked="True" IsThreeState="True">Three State: Checked</CheckBox> <CheckBox IsChecked="True" IsThreeState="True">Three State: Checked</CheckBox>
<CheckBox IsChecked="{x:Null}" IsThreeState="True">Three State: Indeterminate</CheckBox> <CheckBox IsChecked="{x:Null}" IsThreeState="True">Three State: Indeterminate</CheckBox>
@ -25,4 +25,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Menu</TextBlock> <TextBlock Classes="h1">Context Menu</TextBlock>
<TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock> <TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Border Background="{DynamicResource ThemeAccentBrush}" <Border Background="{DynamicResource ThemeAccentBrush}"
Padding="48,48,48,48"> Padding="48,48,48,48">
<Border.ContextMenu> <Border.ContextMenu>
@ -33,4 +33,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/DatePickerPage.xaml

@ -1,13 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">DatePicker</TextBlock> <TextBlock Classes="h1">DatePicker</TextBlock>
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock> <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Width="200"> Width="200">
<TextBlock Text="SelectedDateFormat: Short"/> <TextBlock Text="SelectedDateFormat: Short"/>
@ -43,4 +43,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

4
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4" Margin="4"> <StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<Button Name="OpenFile">Open File</Button> <Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button> <Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button> <Button Name="SelectFolder">Select Folder</Button>
@ -9,4 +9,4 @@
</StackPanel> </StackPanel>
<Button Name="DecoratedWindow">Decorated window</Button> <Button Name="DecoratedWindow">Decorated window</Button>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Drag+Drop</TextBlock> <TextBlock Classes="h1">Drag+Drop</TextBlock>
<TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock> <TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe"> <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
<TextBlock Name="DragState">Drag Me</TextBlock> <TextBlock Name="DragState">Drag Me</TextBlock>
</Border> </Border>
@ -16,4 +16,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/DropDownPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">DropDown</TextBlock> <TextBlock Classes="h1">DropDown</TextBlock>
<TextBlock Classes="h2">A drop-down list.</TextBlock> <TextBlock Classes="h2">A drop-down list.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<DropDown SelectedIndex="0"> <DropDown SelectedIndex="0">
<DropDownItem>Inline Items</DropDownItem> <DropDownItem>Inline Items</DropDownItem>
<DropDownItem>Inline Item 2</DropDownItem> <DropDownItem>Inline Item 2</DropDownItem>
@ -28,4 +28,4 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Expander</TextBlock> <TextBlock Classes="h1">Expander</TextBlock>
<TextBlock Classes="h2">Expands to show nested content</TextBlock> <TextBlock Classes="h2">Expands to show nested content</TextBlock>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Expander Header="Expand Up" ExpandDirection="Up"> <Expander Header="Expand Up" ExpandDirection="Up">
<StackPanel> <StackPanel>
<TextBlock>Expanded content</TextBlock> <TextBlock>Expanded content</TextBlock>
@ -29,4 +29,4 @@
</Expander> </Expander>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

4
samples/ControlCatalog/Pages/ImagePage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Image</TextBlock> <TextBlock Classes="h1">Image</TextBlock>
<TextBlock Classes="h2">Displays an image</TextBlock> <TextBlock Classes="h2">Displays an image</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock>No Stretch</TextBlock> <TextBlock>No Stretch</TextBlock>
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg" <Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg"

4
samples/ControlCatalog/Pages/MenuPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Menu</TextBlock> <TextBlock Classes="h1">Menu</TextBlock>
<TextBlock Classes="h2">A window menu</TextBlock> <TextBlock Classes="h2">A window menu</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<Menu> <Menu>
<MenuItem Header="_First"> <MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/> <MenuItem Header="Standard _Menu Item"/>

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -1,6 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock> <TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock> <TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
@ -26,7 +26,7 @@
VerticalAlignment="Center" Margin="2"> VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate> <DropDown.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2"> <StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/> <TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/> <TextBlock Text="{Binding Value}"/>
@ -69,7 +69,7 @@
</Grid> </Grid>
</Grid> </Grid>
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10"> <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock> <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100" CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"

6
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ProgressBar</TextBlock> <TextBlock Classes="h1">ProgressBar</TextBlock>
<TextBlock Classes="h2">A progress bar control</TextBlock> <TextBlock Classes="h2">A progress bar control</TextBlock>
@ -7,8 +7,8 @@
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Gap="16"> <StackPanel Spacing="16">
<ProgressBar Value="{Binding #hprogress.Value}" /> <ProgressBar Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="True"/> <ProgressBar IsIndeterminate="True"/>
</StackPanel> </StackPanel>

8
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@ -1,22 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">RadioButton</TextBlock> <TextBlock Classes="h1">RadioButton</TextBlock>
<TextBlock Classes="h2">Allows the selection of a single option of many</TextBlock> <TextBlock Classes="h2">Allows the selection of a single option of many</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Gap="16"> Spacing="16">
<RadioButton IsChecked="True">Option 1</RadioButton> <RadioButton IsChecked="True">Option 1</RadioButton>
<RadioButton>Option 2</RadioButton> <RadioButton>Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Option 3</RadioButton> <RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
<RadioButton IsEnabled="False">Disabled</RadioButton> <RadioButton IsEnabled="False">Disabled</RadioButton>
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Gap="16"> Spacing="16">
<RadioButton IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton> <RadioButton IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton>
<RadioButton IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton> <RadioButton IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}" IsThreeState="True">Three States: Option 3</RadioButton> <RadioButton IsChecked="{x:Null}" IsThreeState="True">Three States: Option 3</RadioButton>

4
samples/ControlCatalog/Pages/SliderPage.xaml

@ -1,9 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Slider</TextBlock> <TextBlock Classes="h1">Slider</TextBlock>
<TextBlock Classes="h2">A control that lets the user select from a range of values by moving a Thumb control along a Track.</TextBlock> <TextBlock Classes="h2">A control that lets the user select from a range of values by moving a Thumb control along a Track.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="16"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="16">
<Slider Value="0" <Slider Value="0"
Minimum="0" Minimum="0"
Maximum="100" Maximum="100"

11
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -1,14 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TextBox</TextBlock> <TextBlock Classes="h1">TextBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock> <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<StackPanel Orientation="Vertical" Gap="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" /> <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
<TextBox Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Watermark" /> <TextBox Width="200" Watermark="Watermark" />
<TextBox Width="200" <TextBox Width="200"
Watermark="Floating Watermark" Watermark="Floating Watermark"
@ -25,13 +26,13 @@
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" /> <TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" Gap="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125" <TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125"
Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." /> Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox AcceptsReturn="True" Width="200" Height="125" <TextBox AcceptsReturn="True" Width="200" Height="125"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." /> Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" Gap="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox Width="200" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/> <TextBox Width="200" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/> <TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-Italic.ttf?assembly=ControlCatalog#Source Sans Pro"/> <TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-Italic.ttf?assembly=ControlCatalog#Source Sans Pro"/>

2
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -1,6 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"
Gap="4"> Spacing="4">
<TextBlock Classes="h1">ToolTip</TextBlock> <TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock> <TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>

4
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -1,12 +1,12 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TreeView</TextBlock> <TextBlock Classes="h1">TreeView</TextBlock>
<TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock> <TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Spacing="16">
<TreeView Items="{Binding}" Width="250" Height="350"> <TreeView Items="{Binding}" Width="250" Height="350">
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}"> <TreeDataTemplate ItemsSource="{Binding Children}">

4
samples/ControlCatalog/SideBar.xaml

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

4
samples/RenderDemo/SideBar.xaml

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

2
samples/VirtualizationDemo/MainWindow.xaml

@ -6,7 +6,7 @@
<StackPanel DockPanel.Dock="Right" <StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0" Margin="16 0 0 0"
MinWidth="150" MinWidth="150"
Gap="4"> Spacing="4">
<DropDown Items="{Binding VirtualizationModes}" <DropDown Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/> SelectedItem="{Binding VirtualizationMode}"/>
<DropDown Items="{Binding Orientations}" <DropDown Items="{Binding Orientations}"

126
src/Avalonia.Animation/Animation.cs

@ -10,13 +10,16 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reflection; using System.Reflection;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Reactive.Linq;
using System.Reactive.Disposables;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Tracks the progress of an animation. /// Tracks the progress of an animation.
/// </summary> /// </summary>
public class Animation : AvaloniaList<KeyFrame>, IDisposable, IAnimation public class Animation : AvaloniaList<KeyFrame>, IAnimation
{ {
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)> private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{ {
@ -24,7 +27,7 @@ namespace Avalonia.Animation
}; };
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition) public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator: IAnimator where TAnimator : IAnimator
{ {
Animators.Insert(0, (condition, typeof(TAnimator))); Animators.Insert(0, (condition, typeof(TAnimator)));
} }
@ -41,8 +44,6 @@ namespace Avalonia.Animation
return null; return null;
} }
private bool _isChildrenChanged = false;
private List<IDisposable> _subscription = new List<IDisposable>();
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>(); public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary> /// <summary>
@ -68,22 +69,18 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// The value fill mode for this animation. /// The value fill mode for this animation.
/// </summary> /// </summary>
public FillMode FillMode { get; set; } public FillMode FillMode { get; set; }
/// <summary> /// <summary>
/// Easing function to be used. /// Easing function to be used.
/// </summary> /// </summary>
public Easing Easing { get; set; } = new LinearEasing(); public Easing Easing { get; set; } = new LinearEasing();
public Animation() private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private void InterpretKeyframes()
{ {
var handlerList = new List<(Type, AvaloniaProperty)>(); var handlerList = new List<(Type type, AvaloniaProperty property)>();
var kfList = new List<AnimatorKeyFrame>(); var animatorKeyFrames = new List<AnimatorKeyFrame>();
var subscriptions = new List<IDisposable>();
foreach (var keyframe in this) foreach (var keyframe in this)
{ {
@ -99,68 +96,87 @@ namespace Avalonia.Animation
if (!handlerList.Contains((handler, setter.Property))) if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property)); handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame() var cue = keyframe.Cue;
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{ {
Handler = handler, cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
Property = setter.Property, }
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime, var newKF = new AnimatorKeyFrame(handler, cue);
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet, subscriptions.Add(newKF.BindSetter(setter, control));
Value = setter.Value
}; animatorKeyFrames.Add(newKF);
kfList.Add(newKF);
} }
} }
var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); var newAnimatorInstances = new List<IAnimator>();
foreach (var handler in handlerList) foreach (var (handlerType, property) in handlerList)
{ {
var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1); var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
newInstance.Property = handler.Item2; newInstance.Property = property;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); newAnimatorInstances.Add(newInstance);
} }
foreach (var kf in kfList) foreach (var keyframe in animatorKeyFrames)
{ {
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
p.prop == kf.Property) a.Property == keyframe.Property);
.First(); animator.Add(keyframe);
parent.inst.Add(kf);
} }
foreach(var instance in newAnimatorInstances) return (newAnimatorInstances, subscriptions);
_animators.Add(instance.inst);
} }
/// <summary> /// <inheritdocs/>
/// Cancels the animation. public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete)
/// </summary>
public void Dispose()
{ {
foreach (var sub in _subscription) var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
}
else
{ {
sub.Dispose(); var completionTasks = onComplete != null ? new List<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
}
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
}
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
}
} }
return new CompositeDisposable(subscriptions);
} }
/// <inheritdocs/> /// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs) public Task RunAsync(Animatable control)
{ {
if (_isChildrenChanged) var run = new TaskCompletionSource<object>();
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators) if (this.RepeatCount == RepeatCount.Loop)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () =>
{ {
_subscription.Add(keyframes.Apply(this, control, matchObs)); run.SetResult(null);
} subscriptions?.Dispose();
return this; });
return run.Task;
} }
} }
} }

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for /// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects. /// <see cref="Animator{T}"/> objects.
/// </summary> /// </summary>
public class AnimatorKeyFrame public class AnimatorKeyFrame : AvaloniaObject
{ {
public Type Handler; public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
public Cue Cue; AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet; public AnimatorKeyFrame()
public AvaloniaProperty Property; {
public object Value;
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
}
public Type AnimatorType { get; }
public Cue Cue { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;
public object Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
}
public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)
{
Property = setter.Property;
var value = setter.Value;
if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
}
}
public T GetTypedValue<T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));
if (Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
}
} }
} }

191
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -35,6 +35,7 @@ namespace Avalonia.Animation
private T _neutralValue; private T _neutralValue;
internal bool _unsubscribe = false; internal bool _unsubscribe = false;
private IObserver<object> _targetObserver; private IObserver<object> _targetObserver;
private readonly Action _onComplete;
[Flags] [Flags]
private enum KeyFramesStates private enum KeyFramesStates
@ -51,9 +52,9 @@ namespace Avalonia.Animation
Disposed Disposed
} }
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes) public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete)
{ {
_parent = keyframes; _parent = animator;
_targetAnimation = animation; _targetAnimation = animation;
_targetControl = control; _targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property); _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -82,6 +83,7 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.DoDelay; _currentState = KeyFramesStates.DoDelay;
else else
_currentState = KeyFramesStates.DoRun; _currentState = KeyFramesStates.DoRun;
_onComplete = onComplete;
} }
public void Step(PlayState _playState, Func<double, T, T> Interpolator) public void Step(PlayState _playState, Func<double, T, T> Interpolator)
@ -123,121 +125,136 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime; double _tempDuration = 0d, _easedTime;
checkstate: bool handled = false;
switch (_currentState)
while (!handled)
{ {
case KeyFramesStates.DoDelay: switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{ {
_targetObserver.OnNext(_firstKFValue); if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
} }
else else
{ {
_targetObserver.OnNext(_lastInterpValue); handled = true;
_delayFrameCount++;
} }
} break;
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
goto checkstate;
}
_delayFrameCount++;
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
goto checkstate;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; case KeyFramesStates.DoRun:
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards: break;
if (_durationFrameCount > _durationTotalFrameCount) case KeyFramesStates.RunForwards:
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; if (_durationFrameCount > _durationTotalFrameCount)
_currentState = KeyFramesStates.RunApplyValue; {
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; }
break;
case KeyFramesStates.RunApplyValue: case KeyFramesStates.RunBackwards:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration); if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
_durationFrameCount++; case KeyFramesStates.RunApplyValue:
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
break; _easedTime = _targetAnimation.Easing.Ease(_tempDuration);
case KeyFramesStates.RunComplete: _durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
handled = true;
break;
if (_checkLoopAndRepeat) case KeyFramesStates.RunComplete:
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping) if (_checkLoopAndRepeat)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{ {
if (_currentIteration >= _repeatCount) _delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{ {
_currentState = KeyFramesStates.Stop; _currentState = KeyFramesStates.DoRun;
} }
else else if (_isRepeating)
{ {
_currentState = KeyFramesStates.DoRun; if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
} }
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse) || _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed; _isReversed = !_isReversed;
break;
}
_currentState = KeyFramesStates.Stop;
break; break;
}
_currentState = KeyFramesStates.Stop; case KeyFramesStates.Stop:
goto checkstate;
case KeyFramesStates.Stop: if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
if (_fillMode == FillMode.Forward _targetObserver.OnCompleted();
|| _fillMode == FillMode.Both) _onComplete?.Invoke();
{ Dispose();
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); handled = true;
} break;
_targetObserver.OnCompleted(); default:
break; handled = true;
break;
}
} }
} }
@ -253,4 +270,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed; _currentState = KeyFramesStates.Disposed;
} }
} }
} }

80
src/Avalonia.Animation/Animator`1.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </summary>
private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>(); private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
private bool _isVerfifiedAndConverted; private bool _isVerfifiedAndConverted;
@ -35,18 +35,17 @@ namespace Avalonia.Animation
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
{ {
if (!_isVerfifiedAndConverted) if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T)); VerifyConvertKeyFrames();
return obsMatch return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused. // Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ => .Subscribe(_ =>
{ {
var timerObs = RunKeyFrames(animation, control); var timerObs = RunKeyFrames(animation, control, onComplete);
}); });
} }
@ -60,8 +59,8 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param> /// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t) protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{ {
KeyValuePair<double, (T, bool)> firstCue, lastCue; KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count(); int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
} }
else else
{ {
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); lastCue = _convertedKeyframes.First(j => j.Key >= t);
} }
} }
else else
@ -89,26 +88,24 @@ namespace Avalonia.Animation
double t0 = firstCue.Key; double t0 = firstCue.Key;
double t1 = lastCue.Key; double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0); var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue)); var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
} }
/// <summary> /// <summary>
/// Runs the KeyFrames Animation. /// Runs the KeyFrames Animation.
/// </summary> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control) private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
{ {
var _kfStateMach = new AnimatorStateMachine<T>(); var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete);
_kfStateMach.Initialize(animation, control, this);
Timing.AnimationStateTimer Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe) .TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => .Subscribe(p => stateMachine.Step(p, DoInterpolation));
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation); return control.Bind(Property, stateMachine, BindingPriority.Animation);
} }
/// <summary> /// <summary>
@ -119,39 +116,19 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Verifies and converts keyframe values according to this class's target type. /// Verifies and converts keyframe values according to this class's target type.
/// </summary> /// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type) private void VerifyConvertKeyFrames()
{ {
var typeConv = TypeDescriptor.GetConverter(type); foreach (AnimatorKeyFrame keyframe in this)
foreach (AnimatorKeyFrame k in this)
{ {
if (k.Value == null) _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
_convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
} }
SortKeyFrameCues(_convertedKeyframes); AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true; _isVerfifiedAndConverted = true;
} }
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues) private void AddNeutralKeyFramesIfNeeded()
{ {
bool hasStartKey, hasEndKey; bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false; hasStartKey = hasEndKey = false;
@ -170,23 +147,20 @@ namespace Avalonia.Animation
} }
if (!hasStartKey || !hasEndKey) if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); AddNeutralKeyFrames(hasStartKey, hasEndKey);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
} }
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes) private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
convertedKeyframes.Add(0.0d, (default(T), true)); _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
if (!hasEndKey) if (!hasEndKey)
{ {
convertedKeyframes.Add(1.0d, (default(T), true)); _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
} }
} }
} }

2
src/Avalonia.Animation/Cue.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// A Cue object for <see cref="KeyFrame"/>. /// A Cue object for <see cref="KeyFrame"/>.
/// </summary> /// </summary>
[TypeConverter(typeof(CueTypeConverter))] [TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double> public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{ {
/// <summary> /// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0 /// The normalized percent value, ranging from 0.0 to 1.0

8
src/Avalonia.Animation/DoubleAnimator.cs

@ -24,15 +24,15 @@ namespace Avalonia.Animation
var firstKF = pair.KFPair.FirstKeyFrame; var firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral) if (firstKF.isNeutral)
y0 = neutralValue; y0 = neutralValue;
else else
y0 = firstKF.Value.TargetValue; y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral) if (secondKF.isNeutral)
y1 = neutralValue; y1 = neutralValue;
else else
y1 = secondKF.Value.TargetValue; y1 = secondKF.TargetValue;
// Do linear parametric interpolation // Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0); return y0 + (pair.IntraKFTime) * (y1 - y0);

10
src/Avalonia.Animation/IAnimation.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -12,6 +13,11 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Apply the animation to the specified control /// Apply the animation to the specified control
/// </summary> /// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match); IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
/// <summary>
/// Run the animation to the specified control
/// </summary>
Task RunAsync(Animatable control);
} }
} }

2
src/Avalonia.Animation/IAnimationSetter.cs

@ -5,4 +5,4 @@ namespace Avalonia.Animation
AvaloniaProperty Property { get; set; } AvaloniaProperty Property { get; set; }
object Value { get; set; } object Value { get; set; }
} }
} }

2
src/Avalonia.Animation/IAnimator.cs

@ -17,6 +17,6 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Applies the current KeyFrame group to the specified control. /// Applies the current KeyFrame group to the specified control.
/// </summary> /// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch); IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete);
} }
} }

16
src/Avalonia.Animation/KeyFrame.cs

@ -7,6 +7,11 @@ using Avalonia.Collections;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
internal enum KeyFrameTimingMode
{
TimeSpan = 1,
Cue
}
/// <summary> /// <summary>
/// Stores data regarding a specific key /// Stores data regarding a specific key
@ -14,7 +19,6 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class KeyFrame : AvaloniaList<IAnimationSetter> public class KeyFrame : AvaloniaList<IAnimationSetter>
{ {
internal bool timeSpanSet, cueSet;
private TimeSpan _ktimeSpan; private TimeSpan _ktimeSpan;
private Cue _kCue; private Cue _kCue;
@ -30,6 +34,8 @@ namespace Avalonia.Animation
{ {
} }
internal KeyFrameTimingMode TimingMode { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the key time of this <see cref="KeyFrame"/>. /// Gets or sets the key time of this <see cref="KeyFrame"/>.
/// </summary> /// </summary>
@ -42,11 +48,11 @@ namespace Avalonia.Animation
} }
set set
{ {
if (cueSet) if (TimingMode == KeyFrameTimingMode.Cue)
{ {
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
} }
timeSpanSet = true; TimingMode = KeyFrameTimingMode.TimeSpan;
_ktimeSpan = value; _ktimeSpan = value;
} }
} }
@ -63,11 +69,11 @@ namespace Avalonia.Animation
} }
set set
{ {
if (timeSpanSet) if (TimingMode == KeyFrameTimingMode.TimeSpan)
{ {
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
} }
cueSet = true; TimingMode = KeyFrameTimingMode.Cue;
_kCue = value; _kCue = value;
} }
} }

8
src/Avalonia.Animation/KeyFramePair`1.cs

@ -22,7 +22,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
/// <param name="FirstKeyFrame"></param> /// <param name="FirstKeyFrame"></param>
/// <param name="LastKeyFrame"></param> /// <param name="LastKeyFrame"></param>
public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this() public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
{ {
this.FirstKeyFrame = FirstKeyFrame; this.FirstKeyFrame = FirstKeyFrame;
this.SecondKeyFrame = LastKeyFrame; this.SecondKeyFrame = LastKeyFrame;
@ -31,11 +31,11 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// First <see cref="KeyFrame"/> object. /// First <see cref="KeyFrame"/> object.
/// </summary> /// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; } public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
/// <summary> /// <summary>
/// Second <see cref="KeyFrame"/> object. /// Second <see cref="KeyFrame"/> object.
/// </summary> /// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; } public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
} }
} }

217
src/Avalonia.Base/AvaloniaObject.cs

@ -10,6 +10,7 @@ using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -21,24 +22,18 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// This class is analogous to DependencyObject in WPF. /// This class is analogous to DependencyObject in WPF.
/// </remarks> /// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{ {
/// <summary> /// <summary>
/// The parent object that inherited values are inherited from. /// The parent object that inherited values are inherited from.
/// </summary> /// </summary>
private IAvaloniaObject _inheritanceParent; private IAvaloniaObject _inheritanceParent;
/// <summary>
/// The set values/bindings on this object.
/// </summary>
private readonly Dictionary<AvaloniaProperty, PriorityValue> _values =
new Dictionary<AvaloniaProperty, PriorityValue>();
/// <summary> /// <summary>
/// Maintains a list of direct property binding subscriptions so that the binding source /// Maintains a list of direct property binding subscriptions so that the binding source
/// doesn't get collected. /// doesn't get collected.
/// </summary> /// </summary>
private List<IDisposable> _directBindings; private List<DirectBindingSubscription> _directBindings;
/// <summary> /// <summary>
/// Event handler for <see cref="INotifyPropertyChanged"/> implementation. /// Event handler for <see cref="INotifyPropertyChanged"/> implementation.
@ -50,20 +45,8 @@ namespace Avalonia
/// </summary> /// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter; private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class. /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -227,9 +210,20 @@ namespace Avalonia
{ {
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
} }
else if (_values != null)
{
var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
else else
{ {
return GetValueInternal(property); return GetDefaultValue(property);
} }
} }
@ -256,7 +250,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; return _values?.IsAnimating(property) ?? false;
} }
/// <summary> /// <summary>
@ -273,14 +267,7 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess(); VerifyAccess();
PriorityValue value; return _values?.IsSet(property) ?? false;
if (_values.TryGetValue(property, out value))
{
return value.Value != AvaloniaProperty.UnsetValue;
}
return false;
} }
/// <summary> /// <summary>
@ -359,36 +346,15 @@ namespace Avalonia
property, property,
description); description);
IDisposable subscription = null;
if (_directBindings == null) if (_directBindings == null)
{ {
_directBindings = new List<IDisposable>(); _directBindings = new List<DirectBindingSubscription>();
} }
subscription = source return new DirectBindingSubscription(this, property, source);
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => SetDirectValue(property, x));
_directBindings.Add(subscription);
return Disposable.Create(() =>
{
subscription.Dispose();
_directBindings.Remove(subscription);
});
} }
else else
{ {
PriorityValue v;
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
_values.Add(property, v);
}
Logger.Verbose( Logger.Verbose(
LogArea.Property, LogArea.Property,
this, this,
@ -397,7 +363,7 @@ namespace Avalonia
description, description,
priority); priority);
return v.Add(source, (int)priority); return Values.AddBinding(property, source, priority);
} }
} }
@ -428,20 +394,11 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property) public void Revalidate(AvaloniaProperty property)
{ {
VerifyAccess(); VerifyAccess();
PriorityValue value; _values?.Revalidate(property);
if (_values.TryGetValue(property, out value))
{
value.Revalidate();
}
} }
/// <inheritdoc/> internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue)
{ {
var property = sender.Property;
var priority = (BindingPriority)sender.ValuePriority;
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) : GetDefaultValue(property) :
oldValue; oldValue;
@ -451,7 +408,7 @@ namespace Avalonia
if (!Equals(oldValue, newValue)) if (!Equals(oldValue, newValue))
{ {
RaisePropertyChanged(property, oldValue, newValue, priority); RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
Logger.Verbose( Logger.Verbose(
LogArea.Property, LogArea.Property,
@ -460,14 +417,13 @@ namespace Avalonia
property, property,
oldValue, oldValue,
newValue, newValue,
priority); (BindingPriority)priority);
} }
} }
/// <inheritdoc/> internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification)
{ {
UpdateDataValidation(sender.Property, notification); UpdateDataValidation(property, notification);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -480,10 +436,7 @@ namespace Avalonia
/// Gets all priority values set on the object. /// Gets all priority values set on the object.
/// </summary> /// </summary>
/// <returns>A collection of property/value tuples.</returns> /// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
{
return _values;
}
/// <summary> /// <summary>
/// Forces revalidation of properties when a property value changes. /// Forces revalidation of properties when a property value changes.
@ -593,12 +546,12 @@ namespace Avalonia
T value) T value)
{ {
Contract.Requires<ArgumentNullException>(setterCallback != null); Contract.Requires<ArgumentNullException>(setterCallback != null);
return DirectPropertyDeferredSetter.SetAndNotify( return Values.Setter.SetAndNotify(
property, property,
ref field, ref field,
(object val, ref T backing, Action<Action> notify) => (object update, ref T backing, Action<Action> notify) =>
{ {
setterCallback((T)val, ref backing, notify); setterCallback((T)update, ref backing, notify);
return true; return true;
}, },
value); value);
@ -672,68 +625,18 @@ namespace Avalonia
} }
} }
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The <see cref="PriorityValue"/>.</returns>
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType());
Func<object, object> validate2 = null;
if (validate != null)
{
validate2 = v => validate(this, v);
}
PriorityValue result = new PriorityValue(
this,
property,
property.PropertyType,
validate2);
return result;
}
/// <summary> /// <summary>
/// Gets the default value for a property. /// Gets the default value for a property.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The default value.</returns> /// <returns>The default value.</returns>
private object GetDefaultValue(AvaloniaProperty property) internal object GetDefaultValue(AvaloniaProperty property)
{ {
if (property.Inherits && InheritanceParent is AvaloniaObject aobj) if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValueInternal(property); return aobj.GetValue(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
} }
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value
/// without check for registered as this can slow getting the value
/// this method is intended for internal usage in AvaloniaObject only
/// it's called only after check the property is registered
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
private object GetValueInternal(AvaloniaProperty property)
{
object result = AvaloniaProperty.UnsetValue;
PriorityValue value;
if (_values.TryGetValue(property, out value))
{
result = value.Value;
}
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
/// <summary> /// <summary>
/// Sets the value of a direct property. /// Sets the value of a direct property.
/// </summary> /// </summary>
@ -814,21 +717,8 @@ namespace Avalonia
originalValue?.GetType().FullName ?? "(null)")); originalValue?.GetType().FullName ?? "(null)"));
} }
PriorityValue v;
if (!_values.TryGetValue(property, out v))
{
if (value == AvaloniaProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
}
LogPropertySet(property, value, priority); LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority); Values.AddValue(property, value, (int)priority);
} }
/// <summary> /// <summary>
@ -908,5 +798,38 @@ namespace Avalonia
value, value,
priority); priority);
} }
private class DirectBindingSubscription : IObserver<object>, IDisposable
{
readonly AvaloniaObject _owner;
readonly AvaloniaProperty _property;
IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
AvaloniaProperty property,
IObservable<object> source)
{
_owner = owner;
_property = property;
_owner._directBindings.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
_owner._directBindings.Remove(this);
}
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(object value)
{
var castValue = CastOrDefault(value, _property.PropertyType);
_owner.SetDirectValue(_property, castValue);
}
}
} }
} }

81
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -36,32 +36,15 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the /// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes. /// object and subsequently each time the property value changes.
/// </returns> /// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property) public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<object>( return new AvaloniaPropertyObservable<object>(o, property);
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(e.NewValue);
}
};
observer.OnNext(o.GetValue(property));
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
} }
/// <summary> /// <summary>
@ -74,51 +57,36 @@ namespace Avalonia
/// An observable which fires immediately with the current value of the property on the /// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes. /// object and subsequently each time the property value changes.
/// </returns> /// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property) public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return o.GetObservable((AvaloniaProperty)property).Cast<T>(); return new AvaloniaPropertyObservable<T>(o, property);
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns> /// <returns>
/// An observable which when subscribed pushes the old and new values of the property each /// An observable which when subscribed pushes the property changed event args
/// time it is changed. Note that the observable returned from this method does not fire /// each time a <see cref="IAvaloniaObject.PropertyChanged"/> event is raised
/// with the current value of the property immediately. /// for the specified property.
/// </returns> /// </returns>
public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>( public static IObservable<AvaloniaPropertyChangedEventArgs> GetPropertyChangedObservable(
this IAvaloniaObject o, this IAvaloniaObject o,
AvaloniaProperty<T> property) AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaObservable<Tuple<T, T>>( return new AvaloniaPropertyChangedObservable(o, property);
observer =>
{
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
o.PropertyChanged += handler;
return Disposable.Create(() =>
{
o.PropertyChanged -= handler;
});
},
GetDescription(o, property));
} }
/// <summary> /// <summary>
@ -166,23 +134,6 @@ namespace Avalonia
o.GetObservable(property)); o.GetObservable(property));
} }
/// <summary>
/// Gets a weak observable for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>An observable.</returns>
public static IObservable<object> GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new WeakPropertyChangedObservable(
new WeakReference<IAvaloniaObject>(o),
property,
GetDescription(o, property));
}
/// <summary> /// <summary>
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>. /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
/// </summary> /// </summary>

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on a type by name.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a registered non-attached property on a type by name. /// Finds a registered property on an object by name.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name); return FindRegistered(o.GetType(), name);
} }
/// <summary>
/// Finds a registered attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
return FindRegisteredAttached(o.GetType(), ownerType, name);
}
/// <summary> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type. /// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary> /// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear(); _attachedCache.Clear();
} }
} }
} }

59
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -2,13 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Collections namespace Avalonia.Collections
@ -43,9 +39,8 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null); Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null); Contract.Requires<ArgumentNullException>(handler != null);
return return collection.GetWeakCollectionChangedObservable()
collection.GetWeakCollectionChangedObservable() .Subscribe(e => handler(collection, e));
.Subscribe(e => handler.Invoke(collection, e));
} }
/// <summary> /// <summary>
@ -63,18 +58,13 @@ namespace Avalonia.Collections
Contract.Requires<ArgumentNullException>(collection != null); Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null); Contract.Requires<ArgumentNullException>(handler != null);
return return collection.GetWeakCollectionChangedObservable().Subscribe(handler);
collection.GetWeakCollectionChangedObservable()
.Subscribe(handler);
} }
private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>, private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs> IWeakSubscriber<NotifyCollectionChangedEventArgs>
{ {
private WeakReference<INotifyCollectionChanged> _sourceReference; private WeakReference<INotifyCollectionChanged> _sourceReference;
private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
private int _count;
public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source) public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
{ {
@ -83,43 +73,28 @@ namespace Avalonia.Collections
public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) public void OnEvent(object sender, NotifyCollectionChangedEventArgs e)
{ {
_changed.OnNext(e); PublishNext(e);
} }
protected override IDisposable SubscribeCore(IObserver<NotifyCollectionChangedEventArgs> observer) protected override void Initialize()
{ {
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{ {
if (_count++ == 0) WeakSubscriptionManager.Subscribe(
{ instance,
WeakSubscriptionManager.Subscribe( nameof(instance.CollectionChanged),
instance, this);
nameof(instance.CollectionChanged),
this);
}
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
} }
} }
private void DecrementCount() protected override void Deinitialize()
{ {
if (--_count == 0) if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
{ {
if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) WeakSubscriptionManager.Unsubscribe(
{ instance,
WeakSubscriptionManager.Unsubscribe( nameof(instance.CollectionChanged),
instance, this);
nameof(instance.CollectionChanged),
this);
}
} }
} }
} }

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

45
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -7,21 +7,23 @@ using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
/// <summary> /// <summary>
/// Binds to an expression on an object using a type value converter to convert the values /// Binds to an expression on an object using a type value converter to convert the values
/// that are send and received. /// that are sent and received.
/// </summary> /// </summary>
public class BindingExpression : ISubject<object>, IDescription public class BindingExpression : LightweightObservableBase<object>, ISubject<object>, IDescription
{ {
private readonly ExpressionObserver _inner; private readonly ExpressionObserver _inner;
private readonly Type _targetType; private readonly Type _targetType;
private readonly object _fallbackValue; private readonly object _fallbackValue;
private readonly BindingPriority _priority; private readonly BindingPriority _priority;
private readonly Subject<object> _errors = new Subject<object>(); InnerListener _innerListener;
WeakReference<object> _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -139,7 +141,7 @@ namespace Avalonia.Data.Core
"IValueConverter should not return non-errored BindingNotification."); "IValueConverter should not return non-errored BindingNotification.");
} }
_errors.OnNext(notification); PublishNext(notification);
if (_fallbackValue != AvaloniaProperty.UnsetValue) if (_fallbackValue != AvaloniaProperty.UnsetValue)
{ {
@ -170,12 +172,18 @@ namespace Avalonia.Data.Core
} }
} }
/// <inheritdoc/> protected override void Initialize() => _innerListener = new InnerListener(this);
public IDisposable Subscribe(IObserver<object> observer) protected override void Deinitialize() => _innerListener.Dispose();
protected override void Subscribed(IObserver<object> observer, bool first)
{ {
return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); if (!first && _value != null && _value.TryGetTarget(out var val) == true)
{
observer.OnNext(val);
}
} }
/// <inheritdoc/>
private object ConvertValue(object value) private object ConvertValue(object value)
{ {
var notification = value as BindingNotification; var notification = value as BindingNotification;
@ -301,5 +309,28 @@ namespace Avalonia.Data.Core
return a; return a;
} }
public class InnerListener : IObserver<object>, IDisposable
{
private readonly BindingExpression _owner;
private readonly IDisposable _dispose;
public InnerListener(BindingExpression owner)
{
_owner = owner;
_dispose = owner._inner.Subscribe(this);
}
public void Dispose() => _dispose.Dispose();
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object value)
{
var converted = _owner.ConvertValue(value);
_owner._value = new WeakReference<object>(converted);
_owner.PublishNext(converted);
}
}
} }
} }

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

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

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

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

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

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

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

@ -3,19 +3,20 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
/// <summary> /// <summary>
/// Observes and sets the value of an expression on an object. /// Observes and sets the value of an expression on an object.
/// </summary> /// </summary>
public class ExpressionObserver : ObservableBase<object>, IDescription public class ExpressionObserver : LightweightObservableBase<object>, IDescription
{ {
/// <summary> /// <summary>
/// An ordered collection of property accessor plugins that can be used to customize /// An ordered collection of property accessor plugins that can be used to customize
@ -54,35 +55,30 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object(); private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private readonly Subject<Unit> _finished; private object _root;
private readonly object _root; private IDisposable _rootSubscription;
private IObservable<object> _result; private WeakReference<object> _value;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="root">The root object.</param> /// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
object root, object root,
string expression, ExpressionNode node,
bool enableDataValidation = false,
string description = null) string description = null)
{ {
Contract.Requires<ArgumentNullException>(expression != null);
if (root == AvaloniaProperty.UnsetValue) if (root == AvaloniaProperty.UnsetValue)
{ {
root = null; root = null;
} }
Expression = expression; _node = node;
Description = description ?? expression; Description = description;
_node = Parse(expression, enableDataValidation);
_root = new WeakReference(root); _root = new WeakReference(root);
} }
@ -90,24 +86,19 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param> /// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
IObservable<object> rootObservable, IObservable<object> rootObservable,
string expression, ExpressionNode node,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootObservable != null); Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_node = node;
Expression = expression; Description = description;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_root = rootObservable; _root = rootObservable;
} }
@ -115,32 +106,92 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootGetter">A function which gets the root object.</param> /// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param> /// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
Func<object> rootGetter, Func<object> rootGetter,
string expression, ExpressionNode node,
IObservable<Unit> update, IObservable<Unit> update,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootGetter != null); Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null); Contract.Requires<ArgumentNullException>(update != null);
Description = description;
Expression = expression; _node = node;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_node.Target = new WeakReference(rootGetter()); _node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter()); _root = update.Select(x => rootGetter());
} }
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
T root,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable.Select(o => (object)o),
Parse(expression, enableDataValidation),
description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation),
update,
description ?? expression.ToString());
}
/// <summary> /// <summary>
/// Attempts to set the value of a property expression. /// Attempts to set the value of a property expression.
/// </summary> /// </summary>
@ -203,77 +254,54 @@ namespace Avalonia.Data.Core
} }
} }
/// <inheritdoc/> protected override void Initialize()
protected override IDisposable SubscribeCore(IObserver<object> observer)
{ {
if (_result == null) _value = null;
{ _node.Subscribe(ValueChanged);
var source = (IObservable<object>)_node; StartRoot();
}
if (_finished != null)
{
source = source.TakeUntil(_finished);
}
_result = Observable.Using(StartRoot, _ => source)
.Select(ToWeakReference)
.Publish(UninitializedValue)
.RefCount()
.Where(x => x != UninitializedValue)
.Select(Translate);
}
return _result.Subscribe(observer); protected override void Deinitialize()
{
_rootSubscription?.Dispose();
_rootSubscription = null;
_node.Unsubscribe();
} }
private static ExpressionNode Parse(string expression, bool enableDataValidation) protected override void Subscribed(IObserver<object> observer, bool first)
{ {
if (!string.IsNullOrWhiteSpace(expression)) if (!first && _value != null && _value.TryGetTarget(out var value))
{ {
return ExpressionNodeBuilder.Build(expression, enableDataValidation); observer.OnNext(value);
}
else
{
return new EmptyExpressionNode();
} }
} }
private static object ToWeakReference(object o) private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{ {
return o is BindingNotification ? o : new WeakReference(o); return ExpressionTreeParser.Parse(expression, enableDataValidation);
} }
private object Translate(object o) private void StartRoot()
{ {
if (o is WeakReference weak) if (_root is IObservable<object> observable)
{ {
return weak.Target; _rootSubscription = observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
x => PublishCompleted(),
() => PublishCompleted());
} }
else if (BindingNotification.ExtractError(o) is MarkupBindingChainException broken) else
{ {
broken.Commit(Description); _node.Target = (WeakReference)_root;
} }
return o;
} }
private IDisposable StartRoot() private void ValueChanged(object value)
{ {
switch (_root) var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
{ broken?.Commit(Description);
case IObservable<object> observable: _value = new WeakReference<object>(value);
return observable.Subscribe( PublishNext(value);
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
_ => _finished.OnNext(Unit.Default),
() => _finished.OnNext(Unit.Default));
case WeakReference weak:
_node.Target = weak;
break;
default:
throw new AvaloniaInternalException("The ExpressionObserver._root member should only be either an observable or WeakReference.");
}
return Disposable.Empty;
} }
} }
} }

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

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

71
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

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

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

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

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

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

199
src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs

@ -1,199 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Data.Core.Parsers
{
internal class ExpressionParser
{
private bool _enableValidation;
public ExpressionParser(bool enableValidation)
{
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
while (!r.End && state != State.End)
{
switch (state)
{
case State.Start:
state = ParseStart(r, nodes);
break;
case State.AfterMember:
state = ParseAfterMember(r, nodes);
break;
case State.BeforeMember:
state = ParseBeforeMember(r, nodes);
break;
case State.AttachedProperty:
state = ParseAttachedProperty(r, nodes);
break;
case State.Indexer:
state = ParseIndexer(r, nodes);
break;
}
}
if (state == State.BeforeMember)
{
throw new ExpressionParseException(r.Position, "Unexpected end of expression.");
}
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault();
}
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
}
return State.End;
}
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
return State.BeforeMember;
}
else if (ParseStreamOperator(r))
{
nodes.Add(new StreamNode());
return State.AfterMember;
}
else if (PeekOpenBracket(r))
{
return State.Indexer;
}
return State.End;
}
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
return State.AttachedProperty;
}
else
{
var identifier = IdentifierParser.Parse(r);
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
return State.End;
}
}
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{
var owner = IdentifierParser.Parse(r);
if (r.End || !r.TakeIf('.'))
{
throw new ExpressionParseException(r.Position, "Invalid attached property name.");
}
var name = IdentifierParser.Parse(r);
if (r.End || !r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation));
return State.AfterMember;
}
private State ParseIndexer(Reader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
if (args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
}
nodes.Add(new IndexerNode(args));
return State.AfterMember;
}
private static bool ParseNot(Reader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(Reader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(Reader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(Reader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(Reader r)
{
return !r.End && r.TakeIf('^');
}
private enum State
{
Start,
AfterMember,
BeforeMember,
AttachedProperty,
Indexer,
End,
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

27
src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs

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

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

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

25
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
{ {
var set = o.GetSetValues(); var set = o.GetSetValues();
PriorityValue value; if (set.TryGetValue(property, out var obj))
if (set.TryGetValue(property, out value))
{ {
return new AvaloniaPropertyValue( if (obj is PriorityValue value)
property, {
o.GetValue(property), return new AvaloniaPropertyValue(
(BindingPriority)value.ValuePriority, property,
value.GetDiagnostic()); o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new AvaloniaPropertyValue(
property,
obj,
BindingPriority.LocalValue,
"Local value");
}
} }
else else
{ {

12
src/Avalonia.Base/IPriorityValueOwner.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -13,22 +14,25 @@ namespace Avalonia
/// <summary> /// <summary>
/// Called when a <see cref="PriorityValue"/>'s value changes. /// Called when a <see cref="PriorityValue"/>'s value changes.
/// </summary> /// </summary>
/// <param name="sender">The source of the change.</param> /// <param name="property">The the property that has changed.</param>
/// <param name="priority">The priority of the value.</param>
/// <param name="oldValue">The old value.</param> /// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param> /// <param name="newValue">The new value.</param>
void Changed(PriorityValue sender, object oldValue, object newValue); void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
/// <summary> /// <summary>
/// Called when a <see cref="BindingNotification"/> is received by a /// Called when a <see cref="BindingNotification"/> is received by a
/// <see cref="PriorityValue"/>. /// <see cref="PriorityValue"/>.
/// </summary> /// </summary>
/// <param name="sender">The source of the change.</param> /// <param name="property">The the property that has changed.</param>
/// <param name="notification">The notification.</param> /// <param name="notification">The notification.</param>
void BindingNotificationReceived(PriorityValue sender, BindingNotification notification); void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary> /// <summary>
/// Ensures that the current thread is the UI thread. /// Ensures that the current thread is the UI thread.
/// </summary> /// </summary>
void VerifyAccess(); void VerifyAccess();
DeferredSetter<object> Setter { get; }
} }
} }

15
src/Avalonia.Base/PriorityValue.cs

@ -21,7 +21,7 @@ namespace Avalonia
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding /// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the /// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the /// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values. /// owner object is fired with the old and new values.
/// </remarks> /// </remarks>
internal class PriorityValue internal class PriorityValue
@ -30,7 +30,6 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>(); private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate; private readonly Func<object, object> _validate;
private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
private (object value, int priority) _value; private (object value, int priority) _value;
/// <summary> /// <summary>
@ -243,12 +242,18 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param> /// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority) private void UpdateValue(object value, int priority)
{ {
delayedSetter.SetAndNotify(this, Owner.Setter.SetAndNotify(Property,
ref _value, ref _value,
UpdateCore, UpdateCore,
(value, priority)); (value, priority));
} }
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
private bool UpdateCore( private bool UpdateCore(
(object value, int priority) update, (object value, int priority) update,
ref (object value, int priority) backing, ref (object value, int priority) backing,
@ -281,12 +286,12 @@ namespace Avalonia
if (notification == null || notification.HasValue) if (notification == null || notification.HasValue)
{ {
notify(() => Owner?.Changed(this, old, Value)); notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
} }
if (notification != null) if (notification != null)
{ {
Owner?.BindingNotificationReceived(this, notification); Owner?.BindingNotificationReceived(Property, notification);
} }
} }
else else

42
src/Avalonia.Base/Reactive/AvaloniaObservable.cs

@ -1,42 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Disposables;
namespace Avalonia.Reactive
{
/// <summary>
/// An <see cref="IObservable{T}"/> with an additional description.
/// </summary>
/// <typeparam name="T">The type of the elements in the sequence.</typeparam>
public class AvaloniaObservable<T> : ObservableBase<T>, IDescription
{
private readonly Func<IObserver<T>, IDisposable> _subscribe;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObservable{T}"/> class.
/// </summary>
/// <param name="subscribe">The subscribe function.</param>
/// <param name="description">The description of the observable.</param>
public AvaloniaObservable(Func<IObserver<T>, IDisposable> subscribe, string description)
{
Contract.Requires<ArgumentNullException>(subscribe != null);
_subscribe = subscribe;
Description = description;
}
/// <summary>
/// Gets the description of the observable.
/// </summary>
public string Description { get; }
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
return _subscribe(observer) ?? Disposable.Empty;
}
}
}

46
src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs

@ -0,0 +1,46 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyChangedObservable :
LightweightObservableBase<AvaloniaPropertyChangedEventArgs>,
IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyChangedObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
PublishNext(e);
}
}
}
}

52
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@ -0,0 +1,52 @@
using System;
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyObservable<T> : LightweightObservableBase<T>, IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
private T _value;
public AvaloniaPropertyObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
_value = (T)target.GetValue(_property);
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
protected override void Subscribed(IObserver<T> observer, bool first)
{
observer.OnNext(_value);
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_value = (T)e.NewValue;
PublishNext(_value);
}
}
}
}

202
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
/// <summary>
/// Lightweight base class for observable implementations.
/// </summary>
/// <typeparam name="T">The observable type.</typeparam>
/// <remarks>
/// <see cref="ObservableBase{T}"/> is rather heavyweight in terms of allocations and memory
/// usage. This class provides a more lightweight base for some internal observable types
/// in the Avalonia framework.
/// </remarks>
public abstract class LightweightObservableBase<T> : IObservable<T>
{
private Exception _error;
private List<IObserver<T>> _observers = new List<IObserver<T>>();
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
var first = false;
for (; ; )
{
if (Volatile.Read(ref _observers) == null)
{
if (_error != null)
{
observer.OnError(_error);
}
else
{
observer.OnCompleted();
}
return Disposable.Empty;
}
lock (this)
{
if (_observers == null)
{
continue;
}
first = _observers.Count == 0;
_observers.Add(observer);
break;
}
}
if (first)
{
Initialize();
}
Subscribed(observer, first);
return new RemoveObserver(this, observer);
}
void Remove(IObserver<T> observer)
{
if (Volatile.Read(ref _observers) != null)
{
lock (this)
{
var observers = _observers;
if (observers != null)
{
observers.Remove(observer);
if (observers.Count == 0)
{
observers.TrimExcess();
}
else
{
return;
}
} else
{
return;
}
}
Deinitialize();
}
}
sealed class RemoveObserver : IDisposable
{
LightweightObservableBase<T> _parent;
IObserver<T> _observer;
public RemoveObserver(LightweightObservableBase<T> parent, IObserver<T> observer)
{
_parent = parent;
Volatile.Write(ref _observer, observer);
}
public void Dispose()
{
var observer = _observer;
Interlocked.Exchange(ref _parent, null)?.Remove(observer);
_observer = null;
}
}
protected abstract void Initialize();
protected abstract void Deinitialize();
protected void PublishNext(T value)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
}
foreach (var observer in observers)
{
observer.OnNext(value);
}
}
}
protected void PublishCompleted()
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnCompleted();
}
Deinitialize();
}
}
protected void PublishError(Exception error)
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
lock (this)
{
if (_observers == null)
{
return;
}
_error = error;
observers = _observers.ToArray();
Volatile.Write(ref _observers, null);
}
foreach (var observer in observers)
{
observer.OnError(error);
}
Deinitialize();
}
}
protected virtual void Subscribed(IObserver<T> observer, bool first)
{
}
}
}

76
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@ -0,0 +1,76 @@
using System;
using Avalonia.Threading;
namespace Avalonia.Reactive
{
public abstract class SingleSubscriberObservableBase<T> : IObservable<T>, IDisposable
{
private Exception _error;
private IObserver<T> _observer;
private bool _completed;
public IDisposable Subscribe(IObserver<T> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
Dispatcher.UIThread.VerifyAccess();
if (_observer != null)
{
throw new InvalidOperationException("The observable can only be subscribed once.");
}
if (_error != null)
{
observer.OnError(_error);
}
else if (_completed)
{
observer.OnCompleted();
}
else
{
_observer = observer;
Subscribed();
}
return this;
}
void IDisposable.Dispose()
{
Unsubscribed();
_observer = null;
}
protected abstract void Unsubscribed();
protected void PublishNext(T value)
{
_observer?.OnNext(value);
}
protected void PublishCompleted()
{
if (_observer != null)
{
_observer.OnCompleted();
_completed = true;
Unsubscribed();
_observer = null;
}
}
protected void PublishError(Exception error)
{
if (_observer != null)
{
_observer.OnError(error);
_error = error;
Unsubscribed();
_observer = null;
}
}
protected abstract void Subscribed();
}
}

85
src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs

@ -1,85 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Utilities;
namespace Avalonia.Reactive
{
internal class WeakPropertyChangedObservable : ObservableBase<object>,
IWeakSubscriber<AvaloniaPropertyChangedEventArgs>, IDescription
{
private WeakReference<IAvaloniaObject> _sourceReference;
private readonly AvaloniaProperty _property;
private readonly Subject<object> _changed = new Subject<object>();
private int _count;
public WeakPropertyChangedObservable(
WeakReference<IAvaloniaObject> source,
AvaloniaProperty property,
string description)
{
_sourceReference = source;
_property = property;
Description = description;
}
public string Description { get; }
public void OnEvent(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_changed.OnNext(e.NewValue);
}
}
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
if (_count++ == 0)
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
observer.OnNext(instance.GetValue(_property));
return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
.Subscribe(observer);
}
else
{
_changed.OnCompleted();
observer.OnCompleted();
return Disposable.Empty;
}
}
private void DecrementCount()
{
if (--_count == 0)
{
IAvaloniaObject instance;
if (_sourceReference.TryGetTarget(out instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.PropertyChanged),
this);
}
}
}
}
}

6
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → src/Avalonia.Base/Utilities/CharacterReader.cs

@ -3,14 +3,14 @@
using System; using System;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Utilities
{ {
internal class Reader public class CharacterReader
{ {
private readonly string _s; private readonly string _s;
private int _i; private int _i;
public Reader(string s) public CharacterReader(string s)
{ {
_s = s; _s = s;
} }

53
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -8,11 +8,10 @@ namespace Avalonia.Utilities
{ {
/// <summary> /// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent. /// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary> /// </summary>
/// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam> /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TProperty, TSetRecord> class DeferredSetter<TSetRecord>
where TProperty: class
{ {
private struct NotifyDisposable : IDisposable private struct NotifyDisposable : IDisposable
{ {
@ -37,29 +36,44 @@ namespace Avalonia.Utilities
{ {
public bool Notifying { get; set; } public bool Notifying { get; set; }
private Queue<TSetRecord> pendingValues; private SingleOrQueue<TSetRecord> pendingValues;
public Queue<TSetRecord> PendingValues public SingleOrQueue<TSetRecord> PendingValues
{ {
get get
{ {
return pendingValues ?? (pendingValues = new Queue<TSetRecord>()); return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
} }
} }
} }
private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>(); private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
}
return status;
}
/// <summary> /// <summary>
/// Mark the property as currently notifying. /// Mark the property as currently notifying.
/// </summary> /// </summary>
/// <param name="property">The property to mark as notifying.</param> /// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns> /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(TProperty property) private NotifyDisposable MarkNotifying(AvaloniaProperty property)
{ {
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); Contract.Requires<InvalidOperationException>(!IsNotifying(property));
return new NotifyDisposable(setRecords.GetOrCreateValue(property)); SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
} }
/// <summary> /// <summary>
@ -67,19 +81,19 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns> /// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(TProperty property) private bool IsNotifying(AvaloniaProperty property)
=> setRecords.TryGetValue(property, out var value) && value.Notifying; => SetRecords.TryGetValue(property, out var value) && value.Notifying;
/// <summary> /// <summary>
/// Add a pending assignment for the property. /// Add a pending assignment for the property.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param> /// <param name="value">The value to assign.</param>
private void AddPendingSet(TProperty property, TSetRecord value) private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{ {
Contract.Requires<InvalidOperationException>(IsNotifying(property)); Contract.Requires<InvalidOperationException>(IsNotifying(property));
setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); GetOrCreateStatus(property).PendingValues.Enqueue(value);
} }
/// <summary> /// <summary>
@ -87,9 +101,9 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property to check.</param> /// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns> /// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(TProperty property) private bool HasPendingSet(AvaloniaProperty property)
{ {
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
} }
/// <summary> /// <summary>
@ -97,9 +111,9 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The property to check.</param> /// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns> /// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(TProperty property) private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{ {
return setRecords.GetOrCreateValue(property).PendingValues.Dequeue(); return GetOrCreateStatus(property).PendingValues.Dequeue();
} }
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
@ -115,7 +129,7 @@ namespace Avalonia.Utilities
/// </param> /// </param>
/// <param name="value">The value to try to set.</param> /// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>( public bool SetAndNotify<TValue>(
TProperty property, AvaloniaProperty property,
ref TValue backing, ref TValue backing,
SetterDelegate<TValue> setterCallback, SetterDelegate<TValue> setterCallback,
TSetRecord value) TSetRecord value)
@ -144,6 +158,7 @@ namespace Avalonia.Utilities
} }
}); });
} }
return updated; return updated;
} }
else if(!object.Equals(value, backing)) else if(!object.Equals(value, backing))

6
src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -4,11 +4,11 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
namespace Avalonia.Data.Core.Parsers namespace Avalonia.Utilities
{ {
internal static class IdentifierParser public static class IdentifierParser
{ {
public static string Parse(Reader r) public static string Parse(CharacterReader r)
{ {
if (IsValidIdentifierStart(r.Peek)) if (IsValidIdentifierStart(r.Peek))
{ {

58
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Utilities
{
/// <summary>
/// FIFO Queue optimized for holding zero or one items.
/// </summary>
/// <typeparam name="T">The type of items held in the queue.</typeparam>
public class SingleOrQueue<T>
{
private T _head;
private Queue<T> _tail;
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
private bool HasTail => _tail != null;
public bool Empty { get; private set; } = true;
public void Enqueue(T value)
{
if (Empty)
{
_head = value;
}
else
{
Tail.Enqueue(value);
}
Empty = false;
}
public T Dequeue()
{
if (Empty)
{
throw new InvalidOperationException("Cannot dequeue from an empty queue!");
}
var result = _head;
if (HasTail && Tail.Count != 0)
{
_head = Tail.Dequeue();
}
else
{
_head = default;
Empty = true;
}
return result;
}
}
}

182
src/Avalonia.Base/ValueStore.cs

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private readonly AvaloniaObject _owner;
private readonly Dictionary<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)
{
_owner.BindingNotificationReceived(property, notification);
}
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
public object GetValue(AvaloniaProperty property)
{
var result = AvaloniaProperty.UnsetValue;
if (_values.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
return result;
}
public bool IsAnimating(AvaloniaProperty property)
{
return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
return false;
}
public void Revalidate(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
}
public void VerifyAccess() => _owner.VerifyAccess();
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
Func<object, object> validate2 = null;
if (validate != null)
{
validate2 = v => validate(_owner, v);
}
return new PriorityValue(
this,
property,
property.PropertyType,
validate2);
}
private object Validate(AvaloniaProperty property, object value)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
if (validate != null && value != AvaloniaProperty.UnsetValue)
{
return validate(_owner, value);
}
return value;
}
private DeferredSetter<object> _defferedSetter;
public DeferredSetter<object> Setter
{
get
{
return _defferedSetter ??
(_defferedSetter = new DeferredSetter<object>());
}
}
}
}

45
src/Avalonia.Controls/AppBuilderBase.cs

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

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

Loading…
Cancel
Save