committed by
GitHub
143 changed files with 4308 additions and 2067 deletions
@ -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 |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="System.Memory" Version="4.5.0" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Text; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Data.Core |
||||
|
{ |
||||
|
public class AvaloniaPropertyAccessorNode : SettableNode |
||||
|
{ |
||||
|
private IDisposable _subscription; |
||||
|
private readonly bool _enableValidation; |
||||
|
private readonly AvaloniaProperty _property; |
||||
|
|
||||
|
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation) |
||||
|
{ |
||||
|
_property = property; |
||||
|
_enableValidation = enableValidation; |
||||
|
} |
||||
|
|
||||
|
public override string Description => PropertyName; |
||||
|
public string PropertyName { get; } |
||||
|
public override Type PropertyType => _property.PropertyType; |
||||
|
|
||||
|
protected override bool SetTargetValueCore(object value, BindingPriority priority) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (Target.IsAlive && Target.Target is IAvaloniaObject obj) |
||||
|
{ |
||||
|
obj.SetValue(_property, value, priority); |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void StartListeningCore(WeakReference reference) |
||||
|
{ |
||||
|
if (reference.Target is IAvaloniaObject obj) |
||||
|
{ |
||||
|
_subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_subscription = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void StopListeningCore() |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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?; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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}."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
internal class ValueStore : IPriorityValueOwner |
||||
|
{ |
||||
|
private readonly AvaloniaObject _owner; |
||||
|
private readonly Dictionary<AvaloniaProperty, object> _values = |
||||
|
new Dictionary<AvaloniaProperty, object>(); |
||||
|
|
||||
|
public ValueStore(AvaloniaObject owner) |
||||
|
{ |
||||
|
_owner = owner; |
||||
|
} |
||||
|
|
||||
|
public IDisposable AddBinding( |
||||
|
AvaloniaProperty property, |
||||
|
IObservable<object> source, |
||||
|
BindingPriority priority) |
||||
|
{ |
||||
|
PriorityValue priorityValue; |
||||
|
|
||||
|
if (_values.TryGetValue(property, out var v)) |
||||
|
{ |
||||
|
priorityValue = v as PriorityValue; |
||||
|
|
||||
|
if (priorityValue == null) |
||||
|
{ |
||||
|
priorityValue = CreatePriorityValue(property); |
||||
|
priorityValue.SetValue(v, (int)BindingPriority.LocalValue); |
||||
|
_values[property] = priorityValue; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
priorityValue = CreatePriorityValue(property); |
||||
|
_values.Add(property, priorityValue); |
||||
|
} |
||||
|
|
||||
|
return priorityValue.Add(source, (int)priority); |
||||
|
} |
||||
|
|
||||
|
public void AddValue(AvaloniaProperty property, object value, int priority) |
||||
|
{ |
||||
|
PriorityValue priorityValue; |
||||
|
|
||||
|
if (_values.TryGetValue(property, out var v)) |
||||
|
{ |
||||
|
priorityValue = v as PriorityValue; |
||||
|
|
||||
|
if (priorityValue == null) |
||||
|
{ |
||||
|
if (priority == (int)BindingPriority.LocalValue) |
||||
|
{ |
||||
|
_values[property] = Validate(property, value); |
||||
|
Changed(property, priority, v, value); |
||||
|
return; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
priorityValue = CreatePriorityValue(property); |
||||
|
priorityValue.SetValue(v, (int)BindingPriority.LocalValue); |
||||
|
_values[property] = priorityValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (value == AvaloniaProperty.UnsetValue) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (priority == (int)BindingPriority.LocalValue) |
||||
|
{ |
||||
|
_values.Add(property, Validate(property, value)); |
||||
|
Changed(property, priority, AvaloniaProperty.UnsetValue, value); |
||||
|
return; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
priorityValue = CreatePriorityValue(property); |
||||
|
_values.Add(property, priorityValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
priorityValue.SetValue(value, priority); |
||||
|
} |
||||
|
|
||||
|
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) |
||||
|
{ |
||||
|
((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification); |
||||
|
} |
||||
|
|
||||
|
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) |
||||
|
{ |
||||
|
((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue); |
||||
|
} |
||||
|
|
||||
|
public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException(); |
||||
|
|
||||
|
public object GetValue(AvaloniaProperty property) |
||||
|
{ |
||||
|
var result = AvaloniaProperty.UnsetValue; |
||||
|
|
||||
|
if (_values.TryGetValue(property, out var value)) |
||||
|
{ |
||||
|
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public bool IsAnimating(AvaloniaProperty property) |
||||
|
{ |
||||
|
return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false; |
||||
|
} |
||||
|
|
||||
|
public bool IsSet(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_values.TryGetValue(property, out var value)) |
||||
|
{ |
||||
|
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public void Revalidate(AvaloniaProperty property) |
||||
|
{ |
||||
|
if (_values.TryGetValue(property, out var value)) |
||||
|
{ |
||||
|
(value as PriorityValue)?.Revalidate(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void VerifyAccess() => _owner.VerifyAccess(); |
||||
|
|
||||
|
private PriorityValue CreatePriorityValue(AvaloniaProperty property) |
||||
|
{ |
||||
|
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); |
||||
|
Func<object, object> validate2 = null; |
||||
|
|
||||
|
if (validate != null) |
||||
|
{ |
||||
|
validate2 = v => validate(_owner, v); |
||||
|
} |
||||
|
|
||||
|
PriorityValue result = new PriorityValue( |
||||
|
this, |
||||
|
property, |
||||
|
property.PropertyType, |
||||
|
validate2); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private object Validate(AvaloniaProperty property, object value) |
||||
|
{ |
||||
|
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); |
||||
|
|
||||
|
if (validate != null && value != AvaloniaProperty.UnsetValue) |
||||
|
{ |
||||
|
return validate(_owner, value); |
||||
|
} |
||||
|
|
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Enum for ExitMode
|
||||
|
/// </summary>
|
||||
|
public enum ExitMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Indicates an implicit call to Application.Exit when the last window closes.
|
||||
|
/// </summary>
|
||||
|
OnLastWindowClose, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates an implicit call to Application.Exit when the main window closes.
|
||||
|
/// </summary>
|
||||
|
OnMainWindowClose, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the application only exits on an explicit call to Application.Exit.
|
||||
|
/// </summary>
|
||||
|
OnExplicitExit |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public class WindowCollection : IReadOnlyList<Window> |
||||
|
{ |
||||
|
private readonly Application _application; |
||||
|
private readonly List<Window> _windows = new List<Window>(); |
||||
|
|
||||
|
public WindowCollection(Application application) |
||||
|
{ |
||||
|
_application = application; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
/// <summary>
|
||||
|
/// Gets the number of elements in the collection.
|
||||
|
/// </summary>
|
||||
|
public int Count => _windows.Count; |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="T:Avalonia.Controls.Window" /> at the specified index.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The <see cref="T:Avalonia.Controls.Window" />.
|
||||
|
/// </value>
|
||||
|
/// <param name="index">The index.</param>
|
||||
|
/// <returns></returns>
|
||||
|
public Window this[int index] => _windows[index]; |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
/// <summary>
|
||||
|
/// Returns an enumerator that iterates through the collection.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// An enumerator that can be used to iterate through the collection.
|
||||
|
/// </returns>
|
||||
|
public IEnumerator<Window> GetEnumerator() |
||||
|
{ |
||||
|
return _windows.GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
/// <summary>
|
||||
|
/// Returns an enumerator that iterates through a collection.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
|
||||
|
/// </returns>
|
||||
|
IEnumerator IEnumerable.GetEnumerator() |
||||
|
{ |
||||
|
return GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds the specified window.
|
||||
|
/// </summary>
|
||||
|
/// <param name="window">The window.</param>
|
||||
|
internal void Add(Window window) |
||||
|
{ |
||||
|
if (window == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_windows.Add(window); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes the specified window.
|
||||
|
/// </summary>
|
||||
|
/// <param name="window">The window.</param>
|
||||
|
internal void Remove(Window window) |
||||
|
{ |
||||
|
if (window == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_windows.Remove(window); |
||||
|
|
||||
|
OnRemoveWindow(window); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Closes all windows and removes them from the underlying collection.
|
||||
|
/// </summary>
|
||||
|
internal void Clear() |
||||
|
{ |
||||
|
while (_windows.Count > 0) |
||||
|
{ |
||||
|
_windows[0].Close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnRemoveWindow(Window window) |
||||
|
{ |
||||
|
if (window == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_application.IsExiting) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
switch (_application.ExitMode) |
||||
|
{ |
||||
|
case ExitMode.OnLastWindowClose: |
||||
|
if (Count == 0) |
||||
|
{ |
||||
|
_application.Exit(); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case ExitMode.OnMainWindowClose: |
||||
|
if (window == _application.MainWindow) |
||||
|
{ |
||||
|
_application.Exit(); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,51 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using Avalonia.Data; |
|
||||
|
|
||||
namespace Avalonia.Markup.Xaml.MarkupExtensions |
|
||||
{ |
|
||||
using System; |
|
||||
using Avalonia.Data.Converters; |
|
||||
using Avalonia.Markup.Data; |
|
||||
using Portable.Xaml.Markup; |
|
||||
|
|
||||
[MarkupExtensionReturnType(typeof(IBinding))] |
|
||||
public class TemplateBindingExtension : MarkupExtension |
|
||||
{ |
|
||||
public TemplateBindingExtension() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public TemplateBindingExtension(string path) |
|
||||
{ |
|
||||
Path = path; |
|
||||
} |
|
||||
|
|
||||
public override object ProvideValue(IServiceProvider serviceProvider) |
|
||||
{ |
|
||||
return new Binding |
|
||||
{ |
|
||||
Converter = Converter, |
|
||||
ElementName = ElementName, |
|
||||
Mode = Mode, |
|
||||
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), |
|
||||
Path = Path ?? string.Empty, |
|
||||
Priority = Priority, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
public IValueConverter Converter { get; set; } |
|
||||
|
|
||||
public string ElementName { get; set; } |
|
||||
|
|
||||
public object FallbackValue { get; set; } |
|
||||
|
|
||||
public BindingMode Mode { get; set; } |
|
||||
|
|
||||
[ConstructorArgument("path")] |
|
||||
public string Path { get; set; } |
|
||||
|
|
||||
public BindingPriority Priority { get; set; } = BindingPriority.TemplatedParent; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,180 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Data |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A XAML binding to a property on a control's templated parent.
|
||||
|
/// </summary>
|
||||
|
public class TemplateBinding : SingleSubscriberObservableBase<object>, |
||||
|
IBinding, |
||||
|
IDescription, |
||||
|
ISubject<object> |
||||
|
{ |
||||
|
private IStyledElement _target; |
||||
|
private Type _targetType; |
||||
|
|
||||
|
public TemplateBinding() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public TemplateBinding(AvaloniaProperty property) |
||||
|
{ |
||||
|
Property = property; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public InstancedBinding Initiate( |
||||
|
IAvaloniaObject target, |
||||
|
AvaloniaProperty targetProperty, |
||||
|
object anchor = null, |
||||
|
bool enableDataValidation = false) |
||||
|
{ |
||||
|
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
|
||||
|
// use the `TemplateBinding` object itself as the instanced binding in order to save
|
||||
|
// allocating a new object. If the binding *is* instantiated more than once (which can
|
||||
|
// happen if it appears in a `Setter` for example, then just make a clone and instantiate
|
||||
|
// that.
|
||||
|
if (_target == null) |
||||
|
{ |
||||
|
_target = (IStyledElement)target; |
||||
|
_targetType = targetProperty?.PropertyType; |
||||
|
|
||||
|
return new InstancedBinding( |
||||
|
this, |
||||
|
Mode == BindingMode.Default ? BindingMode.OneWay : Mode, |
||||
|
BindingPriority.TemplatedParent); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var clone = new TemplateBinding |
||||
|
{ |
||||
|
Converter = Converter, |
||||
|
ConverterParameter = ConverterParameter, |
||||
|
Property = Property, |
||||
|
}; |
||||
|
|
||||
|
return clone.Initiate(target, targetProperty, anchor, enableDataValidation); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the <see cref="IValueConverter"/> to use.
|
||||
|
/// </summary>
|
||||
|
public IValueConverter Converter { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
|
||||
|
/// </summary>
|
||||
|
public object ConverterParameter { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the binding mode.
|
||||
|
/// </summary>
|
||||
|
public BindingMode Mode { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the name of the source property on the templated parent.
|
||||
|
/// </summary>
|
||||
|
public AvaloniaProperty Property { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public string Description => "TemplateBinding: " + Property; |
||||
|
|
||||
|
void IObserver<object>.OnCompleted() => throw new NotImplementedException(); |
||||
|
void IObserver<object>.OnError(Exception error) => throw new NotImplementedException(); |
||||
|
|
||||
|
void IObserver<object>.OnNext(object value) |
||||
|
{ |
||||
|
if (_target.TemplatedParent != null && Property != null) |
||||
|
{ |
||||
|
if (Converter != null) |
||||
|
{ |
||||
|
value = Converter.ConvertBack( |
||||
|
value, |
||||
|
Property.PropertyType, |
||||
|
ConverterParameter, |
||||
|
CultureInfo.CurrentCulture); |
||||
|
} |
||||
|
|
||||
|
// Use LocalValue priority here, as TemplatedParent doesn't make sense on controls
|
||||
|
// that aren't template children.
|
||||
|
_target.TemplatedParent.SetValue(Property, value, BindingPriority.LocalValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() |
||||
|
{ |
||||
|
TemplatedParentChanged(); |
||||
|
_target.PropertyChanged += TargetPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
protected override void Unsubscribed() |
||||
|
{ |
||||
|
if (_target.TemplatedParent != null) |
||||
|
{ |
||||
|
_target.TemplatedParent.PropertyChanged -= TemplatedParentPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
_target.PropertyChanged -= TargetPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
private void PublishValue() |
||||
|
{ |
||||
|
if (_target.TemplatedParent != null) |
||||
|
{ |
||||
|
var value = Property != null ? |
||||
|
_target.TemplatedParent.GetValue(Property) : |
||||
|
_target.TemplatedParent; |
||||
|
|
||||
|
if (Converter != null) |
||||
|
{ |
||||
|
value = Converter.Convert(value, _targetType, ConverterParameter, CultureInfo.CurrentCulture); |
||||
|
} |
||||
|
|
||||
|
PublishNext(value); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
PublishNext(AvaloniaProperty.UnsetValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void TemplatedParentChanged() |
||||
|
{ |
||||
|
if (_target.TemplatedParent != null) |
||||
|
{ |
||||
|
_target.TemplatedParent.PropertyChanged += TemplatedParentPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
PublishValue(); |
||||
|
} |
||||
|
|
||||
|
private void TargetPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Property == StyledElement.TemplatedParentProperty) |
||||
|
{ |
||||
|
var oldValue = (IAvaloniaObject)e.OldValue; |
||||
|
var newValue = (IAvaloniaObject)e.OldValue; |
||||
|
|
||||
|
if (oldValue != null) |
||||
|
{ |
||||
|
oldValue.PropertyChanged -= TemplatedParentPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
TemplatedParentChanged(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void TemplatedParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Property == Property) |
||||
|
{ |
||||
|
PublishValue(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
{ |
{ |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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() { } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue