Browse Source

Merge branch 'master' into ValueStoreDeferredSetter

pull/1732/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
eae2d4e25d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 156
      .editorconfig
  2. 1
      Avalonia.sln
  3. 2
      readme.md
  4. 60
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  5. 2
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  6. 2
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  7. 30
      src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs
  8. 130
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  9. 4
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  10. 71
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  11. 92
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  12. 2
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  13. 27
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  14. 219
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  15. 30
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  16. 2
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  17. 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  18. 27
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  19. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  20. 20
      src/Avalonia.Controls/Grid.cs
  21. 7
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  22. 19
      src/Avalonia.Controls/StackPanel.cs
  23. 2
      src/Avalonia.Controls/TextBox.cs
  24. 11
      src/Avalonia.Styling/Styling/Setter.cs
  25. 5
      src/Avalonia.Themes.Default/MenuItem.xaml
  26. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs
  27. 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  28. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  29. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  30. 35
      src/Markup/Avalonia.Markup/Data/Binding.cs
  31. 6
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  32. 3
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  33. 75
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  34. 35
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  35. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs
  36. 88
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  37. 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs
  38. 4
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  39. 10
      src/OSX/Avalonia.MonoMac/WindowImpl.cs
  40. 4
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  41. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  42. 18
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  43. 43
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  44. 30
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs
  45. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs
  46. 32
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  47. 224
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs
  48. 89
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  49. 17
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs
  50. 97
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs
  51. 24
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  52. 120
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  53. 29
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs
  54. 15
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs
  55. 28
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  56. 39
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  57. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  58. 8
      tests/Avalonia.LeakTests/ExpressionObserverTests.cs
  59. 124
      tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs
  60. 47
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  61. 23
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs
  62. 165
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  63. 59
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  64. 371
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs
  65. 13
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs
  66. 112
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
  67. 42
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
  68. 2
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  69. 2
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  70. 18
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  71. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_NullSource.expected.png
  72. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_NullSource.expected.png

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

1
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

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:

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

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

@ -6,7 +6,7 @@ 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 => ".";
} }

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

@ -5,7 +5,7 @@ using System;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal abstract class ExpressionNode 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 =

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

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

@ -3,9 +3,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive; using Avalonia.Reactive;
@ -61,27 +63,22 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="root">The root object.</param> /// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
object root, object root,
string expression, ExpressionNode node,
bool enableDataValidation = false,
string description = null) string description = null)
{ {
Contract.Requires<ArgumentNullException>(expression != null);
if (root == AvaloniaProperty.UnsetValue) if (root == AvaloniaProperty.UnsetValue)
{ {
root = null; root = null;
} }
Expression = expression; _node = node;
Description = description ?? expression; Description = description;
_node = Parse(expression, enableDataValidation);
_root = new WeakReference(root); _root = new WeakReference(root);
} }
@ -89,23 +86,19 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param> /// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
IObservable<object> rootObservable, IObservable<object> rootObservable,
string expression, ExpressionNode node,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootObservable != null); Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_node = node;
Expression = expression; Description = description;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_root = rootObservable; _root = rootObservable;
} }
@ -113,30 +106,92 @@ namespace Avalonia.Data.Core
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary> /// </summary>
/// <param name="rootGetter">A function which gets the root object.</param> /// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param> /// <param name="node">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param> /// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description"> /// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used. /// A description of the expression.
/// </param> /// </param>
public ExpressionObserver( public ExpressionObserver(
Func<object> rootGetter, Func<object> rootGetter,
string expression, ExpressionNode node,
IObservable<Unit> update, IObservable<Unit> update,
bool enableDataValidation = false, string description)
string description = null)
{ {
Contract.Requires<ArgumentNullException>(rootGetter != null); Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null); Contract.Requires<ArgumentNullException>(update != null);
Description = description;
Expression = expression; _node = node;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_node.Target = new WeakReference(rootGetter()); _node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter()); _root = update.Select(x => rootGetter());
} }
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
T root,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable.Select(o => (object)o),
Parse(expression, enableDataValidation),
description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation),
update,
description ?? expression.ToString());
}
/// <summary> /// <summary>
/// Attempts to set the value of a property expression. /// Attempts to set the value of a property expression.
/// </summary> /// </summary>
@ -221,16 +276,9 @@ namespace Avalonia.Data.Core
} }
} }
private static ExpressionNode Parse(string expression, bool enableDataValidation) private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{ {
if (!string.IsNullOrWhiteSpace(expression)) return ExpressionTreeParser.Parse(expression, enableDataValidation);
{
return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
else
{
return new EmptyExpressionNode();
}
} }
private void StartRoot() private void StartRoot()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -8,7 +8,7 @@ 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;

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

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

@ -6,7 +6,7 @@ using System.Reactive.Linq;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class StreamNode : ExpressionNode public class StreamNode : ExpressionNode
{ {
private IDisposable _subscription; private IDisposable _subscription;

20
src/Avalonia.Controls/Grid.cs

@ -194,6 +194,16 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private GridLayout.MeasureResult _rowMeasureCache; private GridLayout.MeasureResult _rowMeasureCache;
/// <summary>
/// Gets the row layout as of the last measure.
/// </summary>
private GridLayout _rowLayoutCache;
/// <summary>
/// Gets the column layout as of the last measure.
/// </summary>
private GridLayout _columnLayoutCache;
/// <summary> /// <summary>
/// Measures the grid. /// Measures the grid.
/// </summary> /// </summary>
@ -253,6 +263,9 @@ namespace Avalonia.Controls
// Cache the measure result and return the desired size. // Cache the measure result and return the desired size.
_columnMeasureCache = columnResult; _columnMeasureCache = columnResult;
_rowMeasureCache = rowResult; _rowMeasureCache = rowResult;
_rowLayoutCache = rowLayout;
_columnLayoutCache = columnLayout;
return new Size(columnResult.DesiredLength, rowResult.DesiredLength); return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once. // Measure each child only once.
@ -299,13 +312,11 @@ namespace Avalonia.Controls
// arrow back to any statements and re-run them without any side-effect. // arrow back to any statements and re-run them without any side-effect.
var (safeColumns, safeRows) = GetSafeColumnRows(); var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = new GridLayout(ColumnDefinitions); var columnLayout = _columnLayoutCache;
var rowLayout = new GridLayout(RowDefinitions); var rowLayout = _rowLayoutCache;
// Calculate for arrange result. // Calculate for arrange result.
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
// Arrange the children. // Arrange the children.
foreach (var child in Children.OfType<Control>()) foreach (var child in Children.OfType<Control>())
{ {
@ -315,7 +326,6 @@ namespace Avalonia.Controls
var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]);
var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]);
var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]);
child.Arrange(new Rect(x, y, width, height)); child.Arrange(new Rect(x, y, width, height));
} }

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

@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (!AlwaysSelected) if (!AlwaysSelected)
{ {
SelectedIndex = -1; selectedIndex = SelectedIndex = -1;
} }
else else
{ {
@ -368,6 +368,11 @@ namespace Avalonia.Controls.Primitives
} }
} }
var items = Items?.Cast<object>();
if (selectedIndex >= items.Count())
{
selectedIndex = SelectedIndex = items.Count() - 1;
}
break; break;
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:

19
src/Avalonia.Controls/StackPanel.cs

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

2
src/Avalonia.Controls/TextBox.cs

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

11
src/Avalonia.Styling/Styling/Setter.cs

@ -158,18 +158,11 @@ namespace Avalonia.Styling
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
} }
case BindingMode.OneWayToSource: default:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger);
}
case BindingMode.TwoWay:
{ {
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
} }
default:
throw new NotSupportedException("Unsupported BindingMode.");
} }
} }

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

@ -122,11 +122,6 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root"> <Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

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

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

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

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

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

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

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

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

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

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

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

@ -100,7 +100,9 @@ namespace Avalonia.Data
CultureInfo.CurrentCulture); CultureInfo.CurrentCulture);
} }
_target.TemplatedParent.SetValue(Property, value, BindingPriority.TemplatedParent); // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls
// that aren't template children.
_target.TemplatedParent.SetValue(Property, value, BindingPriority.LocalValue);
} }
} }
@ -171,7 +173,7 @@ namespace Avalonia.Data
{ {
if (e.Property == Property) if (e.Property == Property)
{ {
PublishNext(_target.TemplatedParent.GetValue(Property)); PublishValue();
} }
} }
} }

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

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

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

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

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

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

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

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

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

@ -12,53 +12,19 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
namespace Avalonia.Data.Core namespace Avalonia.Markup.Parsers.Nodes
{ {
internal class IndexerNode : SettableNode internal class StringIndexerNode : IndexerNodeBase
{ {
private IDisposable _subscription; public StringIndexerNode(IList<string> arguments)
public IndexerNode(IList<string> arguments)
{ {
Arguments = arguments; Arguments = arguments;
} }
public override string Description => "[" + string.Join(",", Arguments) + "]"; public override string Description => "[" + string.Join(",", Arguments) + "]";
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 override bool SetTargetValueCore(object value, BindingPriority priority) protected override bool SetTargetValueCore(object value, BindingPriority priority)
{ {
var typeInfo = Target.Target.GetType().GetTypeInfo(); var typeInfo = Target.Target.GetType().GetTypeInfo();
@ -163,7 +129,7 @@ namespace Avalonia.Data.Core
public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
private object GetValue(object target) protected override object GetValue(object target)
{ {
var typeInfo = target.GetType().GetTypeInfo(); var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList; var list = target as IList;
@ -316,45 +282,19 @@ namespace Avalonia.Data.Core
} }
} }
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{ {
if (sender is IList) var typeInfo = sender.GetType().GetTypeInfo();
{ return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return false;
}
var index = (int)indexObject;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
return index >= e.NewStartingIndex;
case NotifyCollectionChangedAction.Remove:
return index >= e.OldStartingIndex;
case NotifyCollectionChangedAction.Replace:
return index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count;
case NotifyCollectionChangedAction.Move:
return (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems.Count);
case NotifyCollectionChangedAction.Reset:
return true;
}
}
return true; // Implementation defined meaning for the index, so just try to update anyway
} }
private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) protected override int? TryGetFirstArgumentAsInt()
{ {
var typeInfo = sender.GetType().GetTypeInfo(); if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; {
return (int?)value;
}
return null;
} }
} }
} }

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

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

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

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

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

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

4
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -491,6 +491,10 @@ namespace Avalonia.Skia
{ {
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
} }
else
{
paint.Color = new SKColor(255, 255, 255, 0);
}
return paintWrapper; return paintWrapper;
} }

2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -394,7 +394,7 @@ namespace Avalonia.Direct2D1.Media
{ {
return new RadialGradientBrushImpl(radialGradientBrush, _renderTarget, destinationSize); return new RadialGradientBrushImpl(radialGradientBrush, _renderTarget, destinationSize);
} }
else if (imageBrush != null) else if (imageBrush?.Source != null)
{ {
return new ImageBrushImpl( return new ImageBrushImpl(
imageBrush, imageBrush,

18
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -13,6 +13,9 @@ namespace Avalonia.Win32
class SystemDialogImpl : ISystemDialogImpl class SystemDialogImpl : ISystemDialogImpl
{ {
private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT;
public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{ {
var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
@ -29,7 +32,7 @@ namespace Avalonia.Win32
uint options; uint options;
frm.GetOptions(out options); frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); options |= (uint)(DefaultDialogOptions);
if (openDialog?.AllowMultiple == true) if (openDialog?.AllowMultiple == true)
options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT; options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT;
frm.SetOptions(options); frm.SetOptions(options);
@ -37,13 +40,16 @@ namespace Avalonia.Win32
var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";
frm.SetDefaultExtension(defaultExtension); frm.SetDefaultExtension(defaultExtension);
frm.SetFileName(dialog.InitialFileName ?? ""); frm.SetFileName(dialog.InitialFileName ?? "");
frm.SetTitle(dialog.Title); frm.SetTitle(dialog.Title ?? "");
var filters = new List<UnmanagedMethods.COMDLG_FILTERSPEC>(); var filters = new List<UnmanagedMethods.COMDLG_FILTERSPEC>();
foreach (var filter in dialog.Filters) if (dialog.Filters != null)
{ {
var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); foreach (var filter in dialog.Filters)
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask }); {
var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask });
}
} }
if (filters.Count == 0) if (filters.Count == 0)
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" }); filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" });
@ -106,7 +112,7 @@ namespace Avalonia.Win32
var frm = (UnmanagedMethods.IFileDialog)unk; var frm = (UnmanagedMethods.IFileDialog)unk;
uint options; uint options;
frm.GetOptions(out options); frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions);
frm.SetOptions(options); frm.SetOptions(options);
if (dialog.InitialDirectory != null) if (dialog.InitialDirectory != null)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

120
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@ -12,6 +12,7 @@ using Avalonia.Data.Core;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Markup.Parsers;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Base.UnitTests.Data.Core
{ {
@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value() public async Task Should_Get_Simple_Property_Value()
{ {
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -33,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Get_Simple_Property_Value_Type() public void Should_Get_Simple_Property_Value_Type()
{ {
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value_Null() public async Task Should_Get_Simple_Property_Value_Null()
{ {
var data = new { Foo = (string)null }; var data = new { Foo = (string)null };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Null(result); Assert.Null(result);
@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_From_Base_Class() public async Task Should_Get_Simple_Property_From_Base_Class()
{ {
var data = new Class3 { Foo = "foo" }; var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -69,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_Null() public async Task Should_Return_BindingNotification_Error_For_Root_Null()
{ {
var data = new Class3 { Foo = "foo" }; var target = ExpressionObserver.Create(default(Class3), o => o.Foo);
var target = new ExpressionObserver(default(object), "Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty), new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
result); result);
GC.KeepAlive(data);
} }
[Fact] [Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
{ {
var data = new Class3 { Foo = "foo" }; var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty), new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
result); result);
GC.KeepAlive(data);
} }
[Fact] [Fact]
public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null() public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
{ {
var data = new Class3 { Foo = "foo" }; var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo);
var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty), new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
result); result);
GC.KeepAlive(data);
} }
[Fact] [Fact]
public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue() public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
{ {
var data = new Class3 { Foo = "foo" }; var target = ExpressionObserver.Create<object, string>(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo);
var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty), new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
result); result);
GC.KeepAlive(data);
} }
[Fact] [Fact]
public async Task Should_Get_Simple_Property_Chain() public async Task Should_Get_Simple_Property_Chain()
{ {
var data = new { Foo = new { Bar = new { Baz = "baz" } } }; var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("baz", result); Assert.Equal("baz", result);
@ -150,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Get_Simple_Property_Chain_Type() public void Should_Get_Simple_Property_Chain_Type()
{ {
var data = new { Foo = new { Bar = new { Baz = "baz" } } }; var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
@ -159,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact] [Fact]
public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
{ {
var data = new { Foo = default(object) }; var data = new { Foo = default(Class1) };
var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.Subscribe(x => result.Add(x));
@ -189,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[] new[]
{ {
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"), new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
}, },
@ -198,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public void Should_Have_Null_ResultType_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
GC.KeepAlive(data);
}
[Fact] [Fact]
public void Should_Track_Simple_Property_Value() public void Should_Track_Simple_Property_Value()
{ {
var data = new Class1 { Foo = "foo" }; var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -232,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
{ {
var data = new Class1 { Bar = "foo" }; var data = new Class1 { Bar = "foo" };
var target = new ExpressionObserver(data, "Bar"); var target = ExpressionObserver.Create(data, o => o.Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -262,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_End_Of_Property_Chain_Changing() public void Should_Track_End_Of_Property_Chain_Changing()
{ {
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -283,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Property_Chain_Changing() public void Should_Track_Property_Chain_Changing()
{ {
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -316,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
} }
}; };
var target = new ExpressionObserver(data, "Next.Next.Bar"); var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -329,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
"bar", "bar",
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"), new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue), AvaloniaProperty.UnsetValue),
"bar" "bar"
@ -349,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
{ {
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -384,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { Foo = "foo" }; var data = new Class1 { Foo = "foo" };
var update = new Subject<Unit>(); var update = new Subject<Unit>();
var target = new ExpressionObserver(() => data.Foo, "", update); var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.Subscribe(x => result.Add(x));
@ -404,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var source = scheduler.CreateColdObservable( var source = scheduler.CreateColdObservable(
OnNext(1, new Class1 { Foo = "foo" }), OnNext(1, new Class1 { Foo = "foo" }),
OnNext(2, new Class1 { Foo = "bar" })); OnNext(2, new Class1 { Foo = "bar" }));
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
using (target.Subscribe(x => result.Add(x))) using (target.Subscribe(x => result.Add(x)))
@ -420,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Subscribing_Multiple_Times_Should_Return_Values_To_All() public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
{ {
var data = new Class1 { Foo = "foo" }; var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result1 = new List<object>(); var result1 = new List<object>();
var result2 = new List<object>(); var result2 = new List<object>();
var result3 = new List<object>(); var result3 = new List<object>();
@ -443,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
{ {
var data = new Class1 { Foo = "foo" }; var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var sub1 = target.Subscribe(x => { }); var sub1 = target.Subscribe(x => { });
var sub2 = target.Subscribe(x => { }); var sub2 = target.Subscribe(x => { });
@ -462,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Set_Simple_Property_Value() public void SetValue_Should_Set_Simple_Property_Value()
{ {
var data = new Class1 { Foo = "foo" }; var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -478,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Set_Property_At_The_End_Of_Chain() public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
{ {
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -494,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Return_False_For_Missing_Property() public void SetValue_Should_Return_False_For_Missing_Property()
{ {
var data = new Class1 { Next = new WithoutBar() }; var data = new Class1 { Next = new WithoutBar() };
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -508,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Notify_New_Value_With_Inpc() public void SetValue_Should_Notify_New_Value_With_Inpc()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.Subscribe(x => result.Add(x));
@ -523,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Notify_New_Value_Without_Inpc() public void SetValue_Should_Notify_New_Value_Without_Inpc()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Bar"); var target = ExpressionObserver.Create(data, o => o.Bar);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.Subscribe(x => result.Add(x));
@ -538,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void SetValue_Should_Return_False_For_Missing_Object() public void SetValue_Should_Return_False_For_Missing_Object()
{ {
var data = new Class1(); var data = new Class1();
var target = new ExpressionObserver(data, "Next.Bar"); var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -555,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var second = new Class1 { Foo = "bar" }; var second = new Class1 { Foo = "bar" };
var root = first; var root = first;
var update = new Subject<Unit>(); var update = new Subject<Unit>();
var target = new ExpressionObserver(() => root, "Foo", update); var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -570,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
"foo", "foo",
"bar", "bar",
new BindingNotification( new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty), new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
BindingErrorType.Error, BindingErrorType.Error,
AvaloniaProperty.UnsetValue) AvaloniaProperty.UnsetValue)
}, },
@ -589,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> run = () => Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{ {
var source = new Class1 { Foo = "foo" }; var source = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
return Tuple.Create(target, new WeakReference(source)); return Tuple.Create(target, new WeakReference(source));
}; };
@ -673,9 +635,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
} }
private Recorded<Notification<object>> OnNext(long time, object value) private Recorded<Notification<T>> OnNext<T>(long time, T value)
{ {
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value)); return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
} }
} }
} }

29
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs

@ -5,6 +5,7 @@ using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Simple_Property_Value() public void Should_Set_Simple_Property_Value()
{ {
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -30,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Set_Value_On_Simple_Property_Chain() public void Should_Set_Value_On_Simple_Property_Chain()
{ {
var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar"); var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
using (target.Subscribe(_ => { })) using (target.Subscribe(_ => { }))
{ {
@ -44,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Not_Try_To_Set_Value_On_Broken_Chain() public void Should_Not_Try_To_Set_Value_On_Broken_Chain()
{ {
var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar"); var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
// Ensure the ExpressionObserver's subscriptions are kept active. // Ensure the ExpressionObserver's subscriptions are kept active.
target.OfType<string>().Subscribe(x => { }); using (target.OfType<string>().Subscribe(x => { }))
{
data.Foo = null; data.Foo = null;
Assert.False(target.SetValue("foo"));
}
Assert.False(target.SetValue("foo"));
} }
/// <summary> /// <summary>
@ -67,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var rootObservable = new BehaviorSubject<Class1>(data); var rootObservable = new BehaviorSubject<Class1>(data);
var target = new ExpressionObserver(rootObservable, "Foo.Bar"); var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar);
target.Subscribe(_ => { }); using (target.Subscribe(_ => { }))
rootObservable.OnNext(null); {
target.SetValue("baz"); rootObservable.OnNext(null);
target.SetValue("baz");
Assert.Equal("bar", data.Foo.Bar);
}
Assert.Equal("bar", data.Foo.Bar);
} }
private class Class1 : NotifyingBase private class Class1 : NotifyingBase

15
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
public class ExpressionObserverTests_Task public class ExpressionObserverTests_Task
{ {
[Fact] [Fact]
public void Should_Not_Get_Task_Result_Without_Modifier_Char() public void Should_Not_Get_Task_Result_Without_StreamBinding()
{ {
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var tcs = new TaskCompletionSource<string>(); var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task }; var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo"); var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var data = new { Foo = Task.FromResult("foo") }; var data = new { Foo = Task.FromResult("foo") };
var target = new ExpressionObserver(data, "Foo^"); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -59,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var tcs = new TaskCompletionSource<Class2>(); var tcs = new TaskCompletionSource<Class2>();
var data = new Class1(tcs.Task); var data = new Class1(tcs.Task);
var target = new ExpressionObserver(data, "Next^.Foo"); var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -79,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var tcs = new TaskCompletionSource<string>(); var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task }; var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^"); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var data = new { Foo = TaskFromException(new NotSupportedException()) }; var data = new { Foo = TaskFromException(new NotSupportedException()) };
var target = new ExpressionObserver(data, "Foo^"); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -130,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var tcs = new TaskCompletionSource<string>(); var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task }; var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^", true); var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));

28
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -149,6 +149,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(1, target.SelectedIndex); Assert.Equal(1, target.SelectedIndex);
} }
[Fact]
public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Selected()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItem = items[2];
Assert.Equal(items[2], target.SelectedItem);
Assert.Equal(2, target.SelectedIndex);
items.RemoveAt(0);
Assert.Equal(items[1], target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
[Fact] [Fact]
public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection() public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection()
{ {

39
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@ -146,5 +146,44 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(20, 0, 30, 120), target.Children[1].Bounds); Assert.Equal(new Rect(20, 0, 30, 120), target.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 50, 120), target.Children[2].Bounds); Assert.Equal(new Rect(50, 0, 50, 120), target.Children[2].Bounds);
} }
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Gap_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new StackPanel
{
Gap = 40,
Orientation = orientation,
Children =
{
new StackPanel { Width = 10, Height= 10, IsVisible = false },
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 },
}
};
var targetTwoChildrenNoneInvisible = new StackPanel
{
Gap = 40,
Orientation = orientation,
Children =
{
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 }
}
};
targetThreeChildrenOneInvisble.Measure(Size.Infinity);
targetThreeChildrenOneInvisble.Arrange(new Rect(targetThreeChildrenOneInvisble.DesiredSize));
targetTwoChildrenNoneInvisible.Measure(Size.Infinity);
targetTwoChildrenNoneInvisible.Arrange(new Rect(targetTwoChildrenNoneInvisible.DesiredSize));
Size sizeWithTwoChildren = targetTwoChildrenNoneInvisible.Bounds.Size;
Size sizeWithThreeChildren = targetThreeChildrenOneInvisble.Bounds.Size;
Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren);
}
} }
} }

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests
public InstancedBinding ItemsSelector(object item) public InstancedBinding ItemsSelector(object item)
{ {
var obs = new ExpressionObserver(item, nameof(Node.Children)); var obs = ExpressionObserver.Create(item, o => (o as Node).Children);
return InstancedBinding.OneWay(obs); return InstancedBinding.OneWay(obs);
} }

8
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

@ -24,7 +24,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () => Func<ExpressionObserver> run = () =>
{ {
var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} }; var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} };
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
return target; return target;
@ -42,7 +42,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () => Func<ExpressionObserver> run = () =>
{ {
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(source, "Foo", true); var target = ExpressionObserver.Create(source, o => o.Foo, true);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
return target; return target;
@ -60,7 +60,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () => Func<ExpressionObserver> run = () =>
{ {
var source = new { Foo = new NonIntegerIndexer() }; var source = new { Foo = new NonIntegerIndexer() };
var target = new ExpressionObserver(source, "Foo"); var target = ExpressionObserver.Create(source, o => o.Foo);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
return target; return target;
@ -78,7 +78,7 @@ namespace Avalonia.LeakTests
Func<ExpressionObserver> run = () => Func<ExpressionObserver> run = () =>
{ {
var source = new { Foo = new MethodBound() }; var source = new { Foo = new MethodBound() };
var target = new ExpressionObserver(source, "Foo.A"); var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
return target; return target;
}; };

124
tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

@ -0,0 +1,124 @@
using System;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class TemplateBindingTests
{
[Fact]
public void OneWay_Binding_Should_Be_Set_Up()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("foo", target.Content);
source.Content = "bar";
Assert.Equal("bar", target.Content);
}
[Fact]
public void TwoWay_Binding_Should_Be_Set_Up()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
{
Mode = BindingMode.TwoWay,
}
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("foo", target.Content);
target.Content = "bar";
Assert.Equal("bar", source.Content);
}
[Fact]
public void Converter_Should_Be_Used()
{
var source = new Button
{
Template = new FuncControlTemplate<Button>(parent =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
{
Mode = BindingMode.TwoWay,
Converter = new PrefixConverter(),
ConverterParameter = "Hello ",
}
}),
};
source.ApplyTemplate();
var target = (ContentPresenter)source.GetVisualChildren().Single();
Assert.Null(target.Content);
source.Content = "foo";
Assert.Equal("Hello foo", target.Content);
target.Content = "Hello bar";
Assert.Equal("bar", source.Content);
}
private class PrefixConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && parameter != null)
{
return parameter.ToString() + value;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && parameter != null)
{
var s = value.ToString();
var prefix = parameter.ToString();
if (s.StartsWith(prefix) == true)
{
return s.Substring(prefix.Length);
}
return s;
}
return null;
}
}
}
}

47
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -4,16 +4,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Parsers.Nodes;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Markup.UnitTests.Parsers
{ {
public class ExpressionNodeBuilderTests public class ExpressionObserverBuilderTests
{ {
[Fact] [Fact]
public void Should_Build_Single_Property() public void Should_Build_Single_Property()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo")); var result = ToList(ExpressionObserverBuilder.Parse("Foo"));
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
} }
@ -21,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Underscored_Property() public void Should_Build_Underscored_Property()
{ {
var result = ToList(ExpressionNodeBuilder.Build("_Foo")); var result = ToList(ExpressionObserverBuilder.Parse("_Foo"));
AssertIsProperty(result[0], "_Foo"); AssertIsProperty(result[0], "_Foo");
} }
@ -29,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Property_With_Digits() public void Should_Build_Property_With_Digits()
{ {
var result = ToList(ExpressionNodeBuilder.Build("F0o")); var result = ToList(ExpressionObserverBuilder.Parse("F0o"));
AssertIsProperty(result[0], "F0o"); AssertIsProperty(result[0], "F0o");
} }
@ -37,7 +39,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Property_Chain() public void Should_Build_Property_Chain()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar.Baz"));
Assert.Equal(3, result.Count); Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
@ -48,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Negated_Property_Chain() public void Should_Build_Negated_Property_Chain()
{ {
var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz")); var result = ToList(ExpressionObserverBuilder.Parse("!Foo.Bar.Baz"));
Assert.Equal(4, result.Count); Assert.Equal(4, result.Count);
Assert.IsType<LogicalNotNode>(result[0]); Assert.IsType<LogicalNotNode>(result[0]);
@ -60,7 +62,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Double_Negated_Property_Chain() public void Should_Build_Double_Negated_Property_Chain()
{ {
var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz")); var result = ToList(ExpressionObserverBuilder.Parse("!!Foo.Bar.Baz"));
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
Assert.IsType<LogicalNotNode>(result[0]); Assert.IsType<LogicalNotNode>(result[0]);
@ -73,29 +75,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Indexed_Property() public void Should_Build_Indexed_Property()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo[15]")); var result = ToList(ExpressionObserverBuilder.Parse("Foo[15]"));
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], "15"); AssertIsIndexer(result[1], "15");
Assert.IsType<IndexerNode>(result[1]); Assert.IsType<StringIndexerNode>(result[1]);
} }
[Fact] [Fact]
public void Should_Build_Indexed_Property_StringIndex() public void Should_Build_Indexed_Property_StringIndex()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo[Key]")); var result = ToList(ExpressionObserverBuilder.Parse("Foo[Key]"));
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], "Key"); AssertIsIndexer(result[1], "Key");
Assert.IsType<IndexerNode>(result[1]); Assert.IsType<StringIndexerNode>(result[1]);
} }
[Fact] [Fact]
public void Should_Build_Multiple_Indexed_Property() public void Should_Build_Multiple_Indexed_Property()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]")); var result = ToList(ExpressionObserverBuilder.Parse("Foo[15,6]"));
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
@ -105,7 +107,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Multiple_Indexed_Property_With_Space() public void Should_Build_Multiple_Indexed_Property_With_Space()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]")); var result = ToList(ExpressionObserverBuilder.Parse("Foo[5, 16]"));
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
@ -115,7 +117,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Consecutive_Indexers() public void Should_Build_Consecutive_Indexers()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]")); var result = ToList(ExpressionObserverBuilder.Parse("Foo[15][16]"));
Assert.Equal(3, result.Count); Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
@ -126,7 +128,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public void Should_Build_Indexed_Property_In_Chain() public void Should_Build_Indexed_Property_In_Chain()
{ {
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz")); var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar[5, 6].Baz"));
Assert.Equal(4, result.Count); Assert.Equal(4, result.Count);
AssertIsProperty(result[0], "Foo"); AssertIsProperty(result[0], "Foo");
@ -135,6 +137,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
AssertIsProperty(result[3], "Baz"); AssertIsProperty(result[3], "Baz");
} }
[Fact]
public void Should_Build_Stream_Node()
{
var result = ToList(ExpressionObserverBuilder.Parse("Foo^"));
Assert.Equal(2, result.Count);
Assert.IsType<StreamNode>(result[1]);
}
private void AssertIsProperty(ExpressionNode node, string name) private void AssertIsProperty(ExpressionNode node, string name)
{ {
Assert.IsType<PropertyAccessorNode>(node); Assert.IsType<PropertyAccessorNode>(node);
@ -145,9 +156,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
private void AssertIsIndexer(ExpressionNode node, params string[] args) private void AssertIsIndexer(ExpressionNode node, params string[] args)
{ {
Assert.IsType<IndexerNode>(node); Assert.IsType<StringIndexerNode>(node);
var e = (IndexerNode)node; var e = (StringIndexerNode)node;
Assert.Equal(e.Arguments.ToArray(), args); Assert.Equal(e.Arguments.ToArray(), args);
} }

23
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs

@ -2,73 +2,74 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Markup.UnitTests.Parsers
{ {
public class ExpressionNodeBuilderTests_Errors public class ExpressionObserverBuilderTests_Errors
{ {
[Fact] [Fact]
public void Identifier_Cannot_Start_With_Digit() public void Identifier_Cannot_Start_With_Digit()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("1Foo")); () => ExpressionObserverBuilder.Parse("1Foo"));
} }
[Fact] [Fact]
public void Identifier_Cannot_Start_With_Symbol() public void Identifier_Cannot_Start_With_Symbol()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.%Bar")); () => ExpressionObserverBuilder.Parse("Foo.%Bar"));
} }
[Fact] [Fact]
public void Expression_Cannot_End_With_Period() public void Expression_Cannot_End_With_Period()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar.")); () => ExpressionObserverBuilder.Parse("Foo.Bar."));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Empty_Indexer() public void Expression_Cannot_Have_Empty_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[]")); () => ExpressionObserverBuilder.Parse("Foo.Bar[]"));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer() public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]")); () => ExpressionObserverBuilder.Parse("Foo.Bar[,3,4]"));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Extra_Comma_In_Indexer() public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]")); () => ExpressionObserverBuilder.Parse("Foo.Bar[3,,4]"));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer() public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]")); () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4,]"));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Digit_After_Indexer() public void Expression_Cannot_Have_Digit_After_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5")); () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]5"));
} }
[Fact] [Fact]
public void Expression_Cannot_Have_Letter_After_Indexer() public void Expression_Cannot_Have_Letter_After_Indexer()
{ {
Assert.Throws<ExpressionParseException>( Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A")); () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]A"));
} }
} }
} }

165
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@ -0,0 +1,165 @@
// 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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_AttachedProperty
{
private readonly Func<string, string, Type> _typeResolver;
public ExpressionObserverBuilderTests_AttachedProperty()
{
var foo = Owner.FooProperty;
_typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null;
}
[Fact]
public async Task Should_Get_Attached_Property_Value()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public async Task Should_Get_Attached_Property_Value_With_Namespace()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(
data,
"(NS:Owner.Foo)",
typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null);
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public async Task Should_Get_Chained_Attached_Property_Value()
{
var data = new Class1
{
Next = new Class1
{
[Owner.FooProperty] = "bar",
}
};
var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
var result = await target.Take(1);
Assert.Equal("bar", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_Simple_Attached_Value()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.SetValue(Owner.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_Chained_Attached_Value()
{
var data = new Class1
{
Next = new Class1
{
[Owner.FooProperty] = "foo",
}
};
var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Next.SetValue(Owner.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Not_Keep_Source_Alive()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new Class1();
var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver);
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
[Fact]
public void Should_Fail_With_Attached_Property_With_Only_1_Part()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver));
}
[Fact]
public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver));
}
private static class Owner
{
public static readonly AttachedProperty<string> FooProperty =
AvaloniaProperty.RegisterAttached<Class1, string>(
"Foo",
typeof(Owner),
defaultValue: "foo");
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<Class1> NextProperty =
AvaloniaProperty.Register<Class1, Class1>(nameof(Next));
public Class1 Next
{
get { return GetValue(NextProperty); }
set { SetValue(NextProperty, value); }
}
}
}
}

59
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs

@ -0,0 +1,59 @@
// 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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_AvaloniaProperty
{
public ExpressionObserverBuilderTests_AvaloniaProperty()
{
var foo = Class1.FooProperty;
}
[Fact]
public async Task Should_Get_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.SetValue(Class1.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string ClrProperty { get; } = "clr-property";
}
}
}

371
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

@ -0,0 +1,371 @@
using Avalonia.Collections;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Parsers.Nodes;
using Avalonia.UnitTests;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Indexer
{
[Fact]
public async Task Should_Get_Array_Value()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_MultiDimensional_Array_Value()
{
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]");
var result = await target.Take(1);
Assert.Equal("qux", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Value_For_String_Indexer()
{
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Value_For_Non_String_Indexer()
{
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_List_Value()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Add()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo.Add("baz");
}
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Remove()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[0]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo.RemoveAt(0);
}
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Replace()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo[1] = "baz";
}
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Move()
{
// Using ObservableCollection here because AvaloniaList does not yet have a Move
// method, but even if it did we need to test with ObservableCollection as well
// as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Foo.Move(0, 1);
Assert.Equal(new[] { "bar", "foo" }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_INCC_Reset()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Foo.Clear();
Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
public void Should_Track_NonIntegerIndexer()
{
var data = new { Foo = new NonIntegerIndexer() };
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
{
data.Foo["foo"] = "bar2";
}
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
public void Should_SetArrayIndex()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
Assert.Equal("baz", data.Foo[1]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Set_ExistingDictionaryEntry()
{
var data = new
{
Foo = new Dictionary<string, int>
{
{"foo", 1 }
}
};
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
}
Assert.Equal(4, data.Foo["foo"]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Add_NewDictionaryEntry()
{
var data = new
{
Foo = new Dictionary<string, int>
{
{"foo", 1 }
}
};
var target = ExpressionObserverBuilder.Build(data, "Foo[bar]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(4));
}
Assert.Equal(4, data.Foo["bar"]);
GC.KeepAlive(data);
}
[Fact]
public void Should_Set_NonIntegerIndexer()
{
var data = new { Foo = new NonIntegerIndexer() };
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("bar2"));
}
Assert.Equal("bar2", data.Foo["foo"]);
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Only_Binding_Works()
{
var data = new[] { 1, 2, 3 };
var target = ExpressionObserverBuilder.Build(data, "[1]");
var value = await target.Take(1);
Assert.Equal(data[1], value);
}
private class NonIntegerIndexer : NotifyingBase
{
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
public string this[string key]
{
get
{
return _storage[key];
}
set
{
_storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
}
}
}

13
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs

@ -1,5 +1,6 @@
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -8,9 +9,9 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Data.Core namespace Avalonia.Markup.UnitTests.Parsers
{ {
public class ExpressionObserverTests_Method public class ExpressionObserverBuilderTests_Method
{ {
private class TestObject private class TestObject
{ {
@ -30,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Method() public async Task Should_Get_Method()
{ {
var data = new TestObject(); var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithoutReturn)); var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithoutReturn));
var result = await observer.Take(1); var result = await observer.Take(1);
Assert.NotNull(result); Assert.NotNull(result);
@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType) public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType)
{ {
var data = new TestObject(); var data = new TestObject();
var observer = new ExpressionObserver(data, methodName); var observer = ExpressionObserverBuilder.Build(data, methodName);
var result = await observer.Take(1); var result = await observer.Take(1);
Assert.IsType(expectedType, result); Assert.IsType(expectedType, result);
@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Can_Call_Method_Returned_From_Observer() public async Task Can_Call_Method_Returned_From_Observer()
{ {
var data = new TestObject(); var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithReturnAndParameters)); var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithReturnAndParameters));
var result = await observer.Take(1); var result = await observer.Take(1);
var callback = (Func<int, int>)result; var callback = (Func<int, int>)result;
@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName) public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
{ {
var data = new TestObject(); var data = new TestObject();
var observer = new ExpressionObserver(data, methodName); var observer = ExpressionObserverBuilder.Build(data, methodName);
var result = await observer.Take(1); var result = await observer.Take(1);
Assert.IsType<BindingNotification>(result); Assert.IsType<BindingNotification>(result);

112
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs

@ -0,0 +1,112 @@
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Negation
{
[Fact]
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
target.Subscribe(_ => { });
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
private class Test
{
public bool Foo { get; set; }
}
}
}

42
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

@ -0,0 +1,42 @@
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Property
{
[Fact]
public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Have_Null_ResultType_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
GC.KeepAlive(data);
}
}
}

2
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -6,7 +6,7 @@ using Avalonia.Markup.Parsers;
using Sprache; using Sprache;
using Xunit; using Xunit;
namespace Avalonia.Markup.UnitTest.Parsers namespace Avalonia.Markup.UnitTests.Parsers
{ {
public class SelectorGrammarTests public class SelectorGrammarTests
{ {

2
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@ -3,7 +3,7 @@ using Avalonia.Controls;
using Avalonia.Markup.Parsers; using Avalonia.Markup.Parsers;
using Xunit; using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Parsers namespace Avalonia.Markup.UnitTests.Parsers
{ {
public class SelectorParserTests public class SelectorParserTests
{ {

18
tests/Avalonia.RenderTests/Media/ImageBrushTests.cs

@ -32,6 +32,24 @@ namespace Avalonia.Direct2D1.RenderTests.Media
get { return System.IO.Path.Combine(OutputPath, "github_icon_small.png"); } get { return System.IO.Path.Combine(OutputPath, "github_icon_small.png"); }
} }
[Fact]
public async Task ImageBrush_NullSource()
{
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new Rectangle
{
Margin = new Thickness(8),
Fill = new ImageBrush()
}
};
await RenderToFile(target);
CompareImages();
}
[Fact] [Fact]
public async Task ImageBrush_Tile_Fill() public async Task ImageBrush_Tile_Fill()
{ {

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_NullSource.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_NullSource.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Loading…
Cancel
Save