103 changed files with 4137 additions and 415 deletions
@ -0,0 +1,30 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using OmniXaml.TypeConversion; |
|||
using Perspex.Input; |
|||
|
|||
namespace Perspex.Markup.Xaml.Converters |
|||
{ |
|||
class KeyGestureConverter : ITypeConverter |
|||
{ |
|||
public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) |
|||
{ |
|||
return KeyGesture.Parse((string)value); |
|||
} |
|||
|
|||
public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
// Copyright (c) The Perspex 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.Subjects; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal abstract class ExpressionNode : IObservable<ExpressionValue> |
|||
{ |
|||
private object _target; |
|||
|
|||
private Subject<ExpressionValue> _subject; |
|||
|
|||
private ExpressionValue _value = ExpressionValue.None; |
|||
|
|||
public ExpressionNode Next { get; set; } |
|||
|
|||
public object Target |
|||
{ |
|||
get { return _target; } |
|||
set |
|||
{ |
|||
if (_target != null) |
|||
{ |
|||
Unsubscribe(_target); |
|||
} |
|||
|
|||
_target = value; |
|||
|
|||
if (_target != null) |
|||
{ |
|||
SubscribeAndUpdate(_target); |
|||
} |
|||
else |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
|
|||
if (Next != null) |
|||
{ |
|||
Next.Target = CurrentValue.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ExpressionValue CurrentValue |
|||
{ |
|||
get |
|||
{ |
|||
return _value; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_value = value; |
|||
|
|||
if (Next != null) |
|||
{ |
|||
Next.Target = value.Value; |
|||
} |
|||
|
|||
if (_subject != null) |
|||
{ |
|||
_subject.OnNext(value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public virtual bool SetValue(object value) |
|||
{ |
|||
return Next?.SetValue(value) ?? false; |
|||
} |
|||
|
|||
public virtual IDisposable Subscribe(IObserver<ExpressionValue> observer) |
|||
{ |
|||
if (Next != null) |
|||
{ |
|||
return Next.Subscribe(observer); |
|||
} |
|||
else |
|||
{ |
|||
if (_subject == null) |
|||
{ |
|||
_subject = new Subject<ExpressionValue>(); |
|||
} |
|||
|
|||
observer.OnNext(CurrentValue); |
|||
return _subject.Subscribe(observer); |
|||
} |
|||
} |
|||
|
|||
protected virtual void SubscribeAndUpdate(object target) |
|||
{ |
|||
CurrentValue = new ExpressionValue(target); |
|||
} |
|||
|
|||
protected virtual void Unsubscribe(object target) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Perspex.Markup.Binding.Parsers; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal static class ExpressionNodeBuilder |
|||
{ |
|||
public static ExpressionNode Build(string expression) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(expression)) |
|||
{ |
|||
throw new ArgumentException("'expression' may not be empty."); |
|||
} |
|||
|
|||
var reader = new Reader(expression); |
|||
var node = ExpressionParser.Parse(reader); |
|||
|
|||
if (!reader.End) |
|||
{ |
|||
throw new ExpressionParseException(reader, "Expected end of expression."); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using System.Reactive.Disposables; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
/// <summary>
|
|||
/// Observes the value of an expression on a root object.
|
|||
/// </summary>
|
|||
public class ExpressionObserver : ObservableBase<ExpressionValue> |
|||
{ |
|||
private int _count; |
|||
private ExpressionNode _node; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="root">The root object.</param>
|
|||
/// <param name="expression">The expression.</param>
|
|||
public ExpressionObserver(object root, string expression) |
|||
{ |
|||
Root = root; |
|||
_node = ExpressionNodeBuilder.Build(expression); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to set the value of a property expression.
|
|||
/// </summary>
|
|||
/// <param name="value">The value to set.</param>
|
|||
/// <returns>
|
|||
/// True if the value could be set; false if the expression does not evaluate to a
|
|||
/// property.
|
|||
/// </returns>
|
|||
public bool SetValue(object value) |
|||
{ |
|||
IncrementCount(); |
|||
|
|||
try |
|||
{ |
|||
return _node.SetValue(value); |
|||
} |
|||
finally |
|||
{ |
|||
DecrementCount(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the root object that the expression is being observed on.
|
|||
/// </summary>
|
|||
public object Root { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override IDisposable SubscribeCore(IObserver<ExpressionValue> observer) |
|||
{ |
|||
IncrementCount(); |
|||
|
|||
var subscription = _node.Subscribe(observer); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
DecrementCount(); |
|||
subscription.Dispose(); |
|||
}); |
|||
} |
|||
|
|||
private void IncrementCount() |
|||
{ |
|||
if (_count++ == 0) |
|||
{ |
|||
_node.Target = Root; |
|||
} |
|||
} |
|||
|
|||
private void DecrementCount() |
|||
{ |
|||
if (--_count == 0) |
|||
{ |
|||
_node.Target = null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Perspex.Markup.Binding.Parsers; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
public class ExpressionParseException : Exception |
|||
{ |
|||
internal ExpressionParseException(int column, string message) |
|||
: base(message) |
|||
{ |
|||
Column = column; |
|||
} |
|||
|
|||
internal ExpressionParseException(Reader r, string message) |
|||
: this(r.Position, message) |
|||
{ |
|||
} |
|||
|
|||
public int Column { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the value for an <see cref="ExpressionObserver"/>.
|
|||
/// </summary>
|
|||
public struct ExpressionValue |
|||
{ |
|||
/// <summary>
|
|||
/// An <see cref="ExpressionValue"/> that has no value.
|
|||
/// </summary>
|
|||
public static readonly ExpressionValue None = new ExpressionValue(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionValue"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="value"></param>
|
|||
public ExpressionValue(object value) |
|||
{ |
|||
HasValue = true; |
|||
Value = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the evaluated expression resulted in a value.
|
|||
/// </summary>
|
|||
public bool HasValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a the result of the expression.
|
|||
/// </summary>
|
|||
public object Value { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
// Copyright (c) The Perspex 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; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class IndexerNode : ExpressionNode |
|||
{ |
|||
private int[] _intArgs; |
|||
|
|||
public IndexerNode(IList<object> arguments) |
|||
{ |
|||
Arguments = arguments; |
|||
|
|||
var intArgs = Arguments.OfType<int>().ToArray(); |
|||
|
|||
if (intArgs.Length == arguments.Count) |
|||
{ |
|||
_intArgs = intArgs; |
|||
} |
|||
} |
|||
|
|||
public IList<object> Arguments { get; } |
|||
|
|||
protected override void SubscribeAndUpdate(object target) |
|||
{ |
|||
CurrentValue = GetValue(target); |
|||
|
|||
var incc = target as INotifyCollectionChanged; |
|||
|
|||
if (incc != null) |
|||
{ |
|||
incc.CollectionChanged += CollectionChanged; |
|||
} |
|||
} |
|||
|
|||
protected override void Unsubscribe(object target) |
|||
{ |
|||
var incc = target as INotifyCollectionChanged; |
|||
|
|||
if (incc != null) |
|||
{ |
|||
incc.CollectionChanged -= CollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
bool update = false; |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
update = _intArgs[0] >= e.NewStartingIndex; |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
update = _intArgs[0] >= e.OldStartingIndex; |
|||
break; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
update = _intArgs[0] >= e.NewStartingIndex && |
|||
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count; |
|||
break; |
|||
case NotifyCollectionChangedAction.Move: |
|||
update = (_intArgs[0] >= e.NewStartingIndex && |
|||
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count) || |
|||
(_intArgs[0] >= e.OldStartingIndex && |
|||
_intArgs[0] < e.OldStartingIndex + e.OldItems.Count); |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
update = true; |
|||
break; |
|||
} |
|||
|
|||
if (update) |
|||
{ |
|||
CurrentValue = GetValue(sender); |
|||
} |
|||
} |
|||
|
|||
private ExpressionValue GetValue(object target) |
|||
{ |
|||
var typeInfo = target.GetType().GetTypeInfo(); |
|||
var list = target as IList; |
|||
|
|||
if (typeInfo.IsArray && _intArgs != null) |
|||
{ |
|||
return new ExpressionValue(((Array)target).GetValue(_intArgs)); |
|||
} |
|||
else if (target is IList && _intArgs?.Length == 1) |
|||
{ |
|||
if (_intArgs[0] < list.Count) |
|||
{ |
|||
return new ExpressionValue(list[_intArgs[0]]); |
|||
} |
|||
} |
|||
|
|||
return ExpressionValue.None; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright (c) The Perspex 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.Globalization; |
|||
using System.Reactive.Linq; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class LogicalNotNode : ExpressionNode |
|||
{ |
|||
public override bool SetValue(object value) |
|||
{ |
|||
throw new NotSupportedException("Cannot set a negated binding."); |
|||
} |
|||
|
|||
public override IDisposable Subscribe(IObserver<ExpressionValue> observer) |
|||
{ |
|||
return Next.Select(x => Negate(x)).Subscribe(observer); |
|||
} |
|||
|
|||
private ExpressionValue Negate(ExpressionValue v) |
|||
{ |
|||
if (v.HasValue) |
|||
{ |
|||
try |
|||
{ |
|||
var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture); |
|||
return new ExpressionValue(!boolean); |
|||
} |
|||
catch |
|||
{ |
|||
// TODO: Maybe should log something here.
|
|||
} |
|||
} |
|||
|
|||
return ExpressionValue.None; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
// Copyright (c) The Perspex 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; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class ArgumentListParser |
|||
{ |
|||
public static IList<object> Parse(Reader r, char open, char close) |
|||
{ |
|||
if (r.Peek == open) |
|||
{ |
|||
var result = new List<object>(); |
|||
|
|||
r.Take(); |
|||
|
|||
while (!r.End) |
|||
{ |
|||
var literal = LiteralParser.Parse(r); |
|||
|
|||
if (literal != null) |
|||
{ |
|||
result.Add(literal); |
|||
} |
|||
else |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected integer."); |
|||
} |
|||
|
|||
r.SkipWhitespace(); |
|||
|
|||
if (r.End) |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ','."); |
|||
} |
|||
else if (r.TakeIf(close)) |
|||
{ |
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
if (r.Take() != ',') |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ','."); |
|||
} |
|||
|
|||
r.SkipWhitespace(); |
|||
} |
|||
} |
|||
|
|||
if (!r.End) |
|||
{ |
|||
r.Take(); |
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ']'."); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class ExpressionParser |
|||
{ |
|||
public static ExpressionNode Parse(Reader r) |
|||
{ |
|||
var nodes = new List<ExpressionNode>(); |
|||
var state = State.Start; |
|||
|
|||
while (!r.End && state != State.End) |
|||
{ |
|||
switch (state) |
|||
{ |
|||
case State.Start: |
|||
state = ParseStart(r, nodes); |
|||
break; |
|||
|
|||
case State.AfterMember: |
|||
state = ParseAfterMember(r, nodes); |
|||
break; |
|||
|
|||
case State.BeforeMember: |
|||
state = ParseBeforeMember(r, nodes); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (state == State.BeforeMember) |
|||
{ |
|||
throw new ExpressionParseException(r, "Unexpected end of expression."); |
|||
} |
|||
|
|||
for (int n = 0; n < nodes.Count - 1; ++n) |
|||
{ |
|||
nodes[n].Next = nodes[n + 1]; |
|||
} |
|||
|
|||
return nodes.FirstOrDefault(); |
|||
} |
|||
|
|||
private static State ParseStart(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseNot(r)) |
|||
{ |
|||
nodes.Add(new LogicalNotNode()); |
|||
return State.Start; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseMemberAccessor(r)) |
|||
{ |
|||
return State.BeforeMember; |
|||
} |
|||
else |
|||
{ |
|||
var args = ArgumentListParser.Parse(r, '[', ']'); |
|||
|
|||
if (args != null) |
|||
{ |
|||
if (args.Count == 0) |
|||
{ |
|||
throw new ExpressionParseException(r, "Indexer may not be empty."); |
|||
} |
|||
|
|||
nodes.Add(new IndexerNode(args)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static bool ParseNot(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('!'); |
|||
} |
|||
|
|||
private static bool ParseMemberAccessor(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('.'); |
|||
} |
|||
|
|||
private enum State |
|||
{ |
|||
Start, |
|||
AfterMember, |
|||
BeforeMember, |
|||
End, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Globalization; |
|||
using System.Text; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class IdentifierParser |
|||
{ |
|||
public static string Parse(Reader r) |
|||
{ |
|||
if (IsValidIdentifierStart(r.Peek)) |
|||
{ |
|||
var result = new StringBuilder(); |
|||
|
|||
while (!r.End && IsValidIdentifierChar(r.Peek)) |
|||
{ |
|||
result.Append(r.Take()); |
|||
} |
|||
|
|||
return result.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private static bool IsValidIdentifierStart(char c) |
|||
{ |
|||
return char.IsLetter(c) || c == '_'; |
|||
} |
|||
|
|||
private static bool IsValidIdentifierChar(char c) |
|||
{ |
|||
if (IsValidIdentifierStart(c)) |
|||
{ |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
var cat = CharUnicodeInfo.GetUnicodeCategory(c); |
|||
return cat == UnicodeCategory.NonSpacingMark || |
|||
cat == UnicodeCategory.SpacingCombiningMark || |
|||
cat == UnicodeCategory.ConnectorPunctuation || |
|||
cat == UnicodeCategory.Format; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Text; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class LiteralParser |
|||
{ |
|||
public static object Parse(Reader r) |
|||
{ |
|||
if (char.IsDigit(r.Peek)) |
|||
{ |
|||
StringBuilder result = new StringBuilder(); |
|||
|
|||
while (!r.End) |
|||
{ |
|||
if (char.IsDigit(r.Peek)) |
|||
{ |
|||
result.Append(r.Take()); |
|||
} |
|||
else |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return int.Parse(result.ToString()); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal class Reader |
|||
{ |
|||
private string _s; |
|||
private int _i; |
|||
|
|||
public Reader(string s) |
|||
{ |
|||
_s = s; |
|||
} |
|||
|
|||
public bool End => _i == _s.Length; |
|||
public char Peek => _s[_i]; |
|||
public int Position => _i; |
|||
public char Take() => _s[_i++]; |
|||
|
|||
public void SkipWhitespace() |
|||
{ |
|||
while (!End && char.IsWhiteSpace(Peek)) |
|||
{ |
|||
Take(); |
|||
} |
|||
} |
|||
|
|||
public bool TakeIf(char c) |
|||
{ |
|||
if (Peek == c) |
|||
{ |
|||
Take(); |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
// Copyright (c) The Perspex 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.ComponentModel; |
|||
using System.Reactive.Linq; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class PropertyAccessorNode : ExpressionNode |
|||
{ |
|||
private PropertyInfo _propertyInfo; |
|||
private IDisposable _subscription; |
|||
|
|||
public PropertyAccessorNode(string propertyName) |
|||
{ |
|||
PropertyName = propertyName; |
|||
} |
|||
|
|||
public string PropertyName { get; } |
|||
|
|||
public override bool SetValue(object value) |
|||
{ |
|||
if (Next != null) |
|||
{ |
|||
return Next.SetValue(value); |
|||
} |
|||
else |
|||
{ |
|||
if (_propertyInfo != null) |
|||
{ |
|||
_propertyInfo.SetValue(Target, value); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
protected override void SubscribeAndUpdate(object target) |
|||
{ |
|||
bool set = false; |
|||
|
|||
if (target != null) |
|||
{ |
|||
_propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName); |
|||
|
|||
if (_propertyInfo != null) |
|||
{ |
|||
ReadValue(target); |
|||
set = true; |
|||
|
|||
var inpc = target as INotifyPropertyChanged; |
|||
|
|||
if (inpc != null) |
|||
{ |
|||
inpc.PropertyChanged += PropertyChanged; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_propertyInfo = null; |
|||
} |
|||
|
|||
if (!set) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
} |
|||
|
|||
protected override void Unsubscribe(object target) |
|||
{ |
|||
var inpc = target as INotifyPropertyChanged; |
|||
|
|||
if (inpc != null) |
|||
{ |
|||
inpc.PropertyChanged -= PropertyChanged; |
|||
} |
|||
} |
|||
|
|||
private void ReadValue(object target) |
|||
{ |
|||
var value = _propertyInfo.GetValue(target); |
|||
var observable = value as IObservable<object>; |
|||
var task = value as Task; |
|||
bool set = false; |
|||
|
|||
if (observable != null) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
set = true; |
|||
_subscription = observable |
|||
.ObserveOn(SynchronizationContext.Current) |
|||
.Subscribe(x => CurrentValue = new ExpressionValue(x)); |
|||
} |
|||
else if (task != null) |
|||
{ |
|||
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); |
|||
|
|||
if (resultProperty != null) |
|||
{ |
|||
if (task.Status == TaskStatus.RanToCompletion) |
|||
{ |
|||
CurrentValue = new ExpressionValue(resultProperty.GetValue(task)); |
|||
set = true; |
|||
} |
|||
else |
|||
{ |
|||
task.ContinueWith( |
|||
x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)), |
|||
TaskScheduler.FromCurrentSynchronizationContext()) |
|||
.ConfigureAwait(false); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
CurrentValue = new ExpressionValue(value); |
|||
set = true; |
|||
} |
|||
|
|||
if (!set) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
} |
|||
|
|||
private void PropertyChanged(object sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == PropertyName) |
|||
{ |
|||
ReadValue(sender); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> |
|||
<PropertyGroup> |
|||
<MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion> |
|||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
|||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> |
|||
<ProjectGuid>{6417E941-21BC-467B-A771-0DE389353CE6}</ProjectGuid> |
|||
<OutputType>Library</OutputType> |
|||
<AppDesignerFolder>Properties</AppDesignerFolder> |
|||
<RootNamespace>Perspex.Markup</RootNamespace> |
|||
<AssemblyName>Perspex.Markup</AssemblyName> |
|||
<DefaultLanguage>en-US</DefaultLanguage> |
|||
<FileAlignment>512</FileAlignment> |
|||
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> |
|||
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile> |
|||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> |
|||
<DebugSymbols>true</DebugSymbols> |
|||
<DebugType>full</DebugType> |
|||
<Optimize>false</Optimize> |
|||
<OutputPath>bin\Debug\</OutputPath> |
|||
<DefineConstants>DEBUG;TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> |
|||
<DebugType>pdbonly</DebugType> |
|||
<Optimize>true</Optimize> |
|||
<OutputPath>bin\Release\</OutputPath> |
|||
<DefineConstants>TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Compile Include="Binding\ExpressionNodeBuilder.cs" /> |
|||
<Compile Include="Binding\ExpressionParseException.cs" /> |
|||
<Compile Include="Binding\ExpressionValue.cs" /> |
|||
<Compile Include="Binding\LogicalNotNode.cs" /> |
|||
<Compile Include="Binding\IndexerNode.cs" /> |
|||
<Compile Include="Binding\Parsers\ArgumentListParser.cs" /> |
|||
<Compile Include="Binding\Parsers\LiteralParser.cs" /> |
|||
<Compile Include="Binding\Parsers\IdentifierParser.cs" /> |
|||
<Compile Include="Binding\Parsers\ExpressionParser.cs" /> |
|||
<Compile Include="Binding\Parsers\Reader.cs" /> |
|||
<Compile Include="Binding\PropertyAccessorNode.cs" /> |
|||
<Compile Include="Binding\ExpressionNode.cs" /> |
|||
<Compile Include="Binding\ExpressionObserver.cs" /> |
|||
<Compile Include="Properties\AssemblyInfo.cs" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Include="packages.config" /> |
|||
</ItemGroup> |
|||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> |
|||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. |
|||
Other similar extension points exist, see Microsoft.Common.targets. |
|||
<Target Name="BeforeBuild"> |
|||
</Target> |
|||
<Target Name="AfterBuild"> |
|||
</Target> |
|||
--> |
|||
</Project> |
|||
@ -0,0 +1,32 @@ |
|||
using System.Resources; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
// General Information about an assembly is controlled through the following
|
|||
// set of attributes. Change these attribute values to modify the information
|
|||
// associated with an assembly.
|
|||
[assembly: AssemblyTitle("Perspex.Markup")] |
|||
[assembly: AssemblyDescription("")] |
|||
[assembly: AssemblyConfiguration("")] |
|||
[assembly: AssemblyCompany("")] |
|||
[assembly: AssemblyProduct("Perspex.Markup")] |
|||
[assembly: AssemblyCopyright("Copyright © 2015")] |
|||
[assembly: AssemblyTrademark("")] |
|||
[assembly: AssemblyCulture("")] |
|||
[assembly: NeutralResourcesLanguage("en")] |
|||
|
|||
// Version information for an assembly consists of the following four values:
|
|||
//
|
|||
// Major Version
|
|||
// Minor Version
|
|||
// Build Number
|
|||
// Revision
|
|||
//
|
|||
// You can specify all the values or you can default the Build and Revision Numbers
|
|||
// by using the '*' as shown below:
|
|||
// [assembly: AssemblyVersion("1.0.*")]
|
|||
[assembly: AssemblyVersion("1.0.0.0")] |
|||
[assembly: AssemblyFileVersion("1.0.0.0")] |
|||
|
|||
[assembly: InternalsVisibleTo("Perspex.Markup.UnitTests")] |
|||
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<packages> |
|||
<package id="Rx-Core" version="2.2.5" targetFramework="portable45-net45+win8" /> |
|||
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable45-net45+win8" /> |
|||
<package id="Rx-Linq" version="2.2.5" targetFramework="portable45-net45+win8" /> |
|||
<package id="Rx-Main" version="2.2.5" targetFramework="portable45-net45+win8" /> |
|||
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable45-net45+win8" /> |
|||
</packages> |
|||
@ -0,0 +1,47 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Perspex |
|||
{ |
|||
/// <summary>
|
|||
/// The priority of a binding.
|
|||
/// </summary>
|
|||
public enum BindingPriority |
|||
{ |
|||
/// <summary>
|
|||
/// A value that comes from an animation.
|
|||
/// </summary>
|
|||
Animation = -1, |
|||
|
|||
/// <summary>
|
|||
/// A local value.
|
|||
/// </summary>
|
|||
LocalValue = 0, |
|||
|
|||
/// <summary>
|
|||
/// A triggered style binding.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A style trigger is a selector such as .class which overrides a
|
|||
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
|
|||
/// for example a Background from the templated parent which changes when the
|
|||
/// control has the :pointerover class.
|
|||
/// </remarks>
|
|||
StyleTrigger, |
|||
|
|||
/// <summary>
|
|||
/// A binding to a property on the templated parent.
|
|||
/// </summary>
|
|||
TemplatedParent, |
|||
|
|||
/// <summary>
|
|||
/// A style binding.
|
|||
/// </summary>
|
|||
Style, |
|||
|
|||
/// <summary>
|
|||
/// The binding is uninitialized.
|
|||
/// </summary>
|
|||
Unset = int.MaxValue, |
|||
} |
|||
} |
|||
@ -1,21 +1,35 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Perspex.Metadata |
|||
{ |
|||
/// <summary>
|
|||
/// Maps an XML namespace to a CLR namespace for use in XAML.
|
|||
/// </summary>
|
|||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] |
|||
public class XmlnsDefinitionAttribute : Attribute |
|||
{ |
|||
public string XmlNamespace { get; set; } |
|||
public string ClrNamespace { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="XmlnsDefinitionAttribute"/> class.
|
|||
/// </summary>
|
|||
/// <param name="xmlNamespace">The URL of the XML namespace.</param>
|
|||
/// <param name="clrNamespace">The CLR namespace.</param>
|
|||
public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace) |
|||
{ |
|||
XmlNamespace = xmlNamespace; |
|||
ClrNamespace = clrNamespace; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the URL of the XML namespace.
|
|||
/// </summary>
|
|||
public string XmlNamespace { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the CLR namespace.
|
|||
/// </summary>
|
|||
public string ClrNamespace { get; set; } |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,117 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using System.Windows.Input; |
|||
using Perspex.Controls; |
|||
using Perspex.Controls.Utils; |
|||
using Perspex.Input; |
|||
|
|||
namespace Perspex.Controls |
|||
{ |
|||
public class HotKeyManager |
|||
{ |
|||
public static PerspexProperty<KeyGesture> HotKeyProperty |
|||
= PerspexProperty.RegisterAttached<Control, KeyGesture>("HotKey", typeof (HotKeyManager)); |
|||
|
|||
class HotkeyCommandWrapper : ICommand |
|||
{ |
|||
public HotkeyCommandWrapper(IControl control) |
|||
{ |
|||
Control = control; |
|||
} |
|||
|
|||
public IControl Control; |
|||
|
|||
private ICommand GetCommand() => Control.GetValue(Button.CommandProperty); |
|||
|
|||
public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false; |
|||
|
|||
public void Execute(object parameter) => GetCommand()?.Execute(parameter); |
|||
|
|||
//Implementation isn't needed in this case
|
|||
public event EventHandler CanExecuteChanged; |
|||
} |
|||
|
|||
|
|||
class Manager |
|||
{ |
|||
private readonly IControl _control; |
|||
private TopLevel _root; |
|||
private IDisposable _parentSub; |
|||
private IDisposable _hotkeySub; |
|||
private KeyGesture _hotkey; |
|||
private HotkeyCommandWrapper _wrapper; |
|||
private KeyBinding _binding; |
|||
|
|||
public Manager(IControl control) |
|||
{ |
|||
_control = control; |
|||
_wrapper = new HotkeyCommandWrapper(_control); |
|||
} |
|||
|
|||
public void Init() |
|||
{ |
|||
_hotkeySub = _control.GetObservable(HotKeyProperty).Subscribe(OnHotkeyChanged); |
|||
_parentSub = AncestorFinder.Create(_control, typeof (TopLevel)).Subscribe(OnParentChanged); |
|||
} |
|||
|
|||
private void OnParentChanged(IControl control) |
|||
{ |
|||
Unregister(); |
|||
_root = (TopLevel) control; |
|||
Register(); |
|||
} |
|||
|
|||
private void OnHotkeyChanged(KeyGesture hotkey) |
|||
{ |
|||
if (hotkey == null) |
|||
//Subscription will be recreated by static property watcher
|
|||
Stop(); |
|||
else |
|||
{ |
|||
Unregister(); |
|||
_hotkey = hotkey; |
|||
Register(); |
|||
} |
|||
} |
|||
|
|||
void Unregister() |
|||
{ |
|||
if (_root != null && _binding != null) |
|||
_root.KeyBindings.Remove(_binding); |
|||
_binding = null; |
|||
} |
|||
|
|||
void Register() |
|||
{ |
|||
if (_root != null && _hotkey != null) |
|||
{ |
|||
_binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; |
|||
_root.KeyBindings.Add(_binding); |
|||
} |
|||
} |
|||
|
|||
void Stop() |
|||
{ |
|||
Unregister(); |
|||
_parentSub.Dispose(); |
|||
_hotkeySub.Dispose(); |
|||
} |
|||
} |
|||
|
|||
static HotKeyManager() |
|||
{ |
|||
HotKeyProperty.Changed.Subscribe(args => |
|||
{ |
|||
var control = args.Sender as IControl; |
|||
if (args.OldValue != null|| control == null) |
|||
return; |
|||
new Manager(control).Init(); |
|||
}); |
|||
} |
|||
public static void SetHotKey(PerspexObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); |
|||
public static KeyGesture GetHotKey(PerspexObject target) => target.GetValue(HotKeyProperty); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Subjects; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Perspex.Controls.Utils |
|||
{ |
|||
public static class AncestorFinder |
|||
{ |
|||
class FinderNode : IDisposable |
|||
{ |
|||
private readonly IControl _control; |
|||
private readonly TypeInfo _ancestorType; |
|||
public IObservable<IControl> Observable => _subject; |
|||
private readonly Subject<IControl> _subject = new Subject<IControl>(); |
|||
|
|||
private FinderNode _child; |
|||
private IDisposable _disposable; |
|||
|
|||
public FinderNode(IControl control, TypeInfo ancestorType) |
|||
{ |
|||
_control = control; |
|||
_ancestorType = ancestorType; |
|||
} |
|||
|
|||
public void Init() |
|||
{ |
|||
_disposable = _control.GetObservable(Control.ParentProperty).Subscribe(OnValueChanged); |
|||
} |
|||
|
|||
private void OnValueChanged(IControl next) |
|||
{ |
|||
if (next == null || _ancestorType.IsAssignableFrom(next.GetType().GetTypeInfo())) |
|||
_subject.OnNext(next); |
|||
else |
|||
{ |
|||
_child?.Dispose(); |
|||
_child = new FinderNode(next, _ancestorType); |
|||
_child.Observable.Subscribe(OnChildValueChanged); |
|||
_child.Init(); |
|||
} |
|||
} |
|||
|
|||
private void OnChildValueChanged(IControl control) => _subject.OnNext(control); |
|||
|
|||
|
|||
public void Dispose() |
|||
{ |
|||
_disposable.Dispose(); |
|||
} |
|||
} |
|||
|
|||
|
|||
public static IObservable<IControl> Create(IControl control, Type ancestorType) |
|||
{ |
|||
return new AnonymousObservable<IControl>(observer => |
|||
{ |
|||
var finder = new FinderNode(control, ancestorType.GetTypeInfo()); |
|||
var subscription = finder.Observable.Subscribe(observer); |
|||
finder.Init(); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
subscription.Dispose(); |
|||
finder.Dispose(); |
|||
}); |
|||
}); |
|||
|
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using System.Windows.Input; |
|||
|
|||
namespace Perspex.Input |
|||
{ |
|||
public class KeyBinding : PerspexObject |
|||
{ |
|||
public static PerspexProperty<ICommand> CommandProperty = |
|||
PerspexProperty.Register<KeyBinding, ICommand>("Command"); |
|||
|
|||
public ICommand Command |
|||
{ |
|||
get { return GetValue(CommandProperty); } |
|||
set { SetValue(CommandProperty, value); } |
|||
} |
|||
|
|||
public static PerspexProperty<KeyGesture> GestureProperty = |
|||
PerspexProperty.Register<KeyBinding, KeyGesture>("Gesture"); |
|||
|
|||
public KeyGesture Gesture |
|||
{ |
|||
get { return GetValue(GestureProperty); } |
|||
set { SetValue(GestureProperty, value); } |
|||
} |
|||
|
|||
public void TryHandle(KeyEventArgs args) |
|||
{ |
|||
if (Gesture?.Matches(args) == true) |
|||
{ |
|||
args.Handled = true; |
|||
if (Command?.CanExecute(null) == true) |
|||
Command.Execute(null); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Perspex.Input |
|||
{ |
|||
public sealed class KeyGesture : IEquatable<KeyGesture> |
|||
{ |
|||
public bool Equals(KeyGesture other) |
|||
{ |
|||
if (ReferenceEquals(null, other)) return false; |
|||
if (ReferenceEquals(this, other)) return true; |
|||
return Key == other.Key && Modifiers == other.Modifiers; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (ReferenceEquals(null, obj)) return false; |
|||
if (ReferenceEquals(this, obj)) return true; |
|||
return obj is KeyGesture && Equals((KeyGesture) obj); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
return ((int) Key*397) ^ (int) Modifiers; |
|||
} |
|||
} |
|||
|
|||
public static bool operator ==(KeyGesture left, KeyGesture right) |
|||
{ |
|||
return Equals(left, right); |
|||
} |
|||
|
|||
public static bool operator !=(KeyGesture left, KeyGesture right) |
|||
{ |
|||
return !Equals(left, right); |
|||
} |
|||
|
|||
public Key Key { get; set; } |
|||
|
|||
public InputModifiers Modifiers { get; set; } |
|||
|
|||
|
|||
static Dictionary<string, Key> KeySynonims = new Dictionary<string, Key> |
|||
{ |
|||
{"+", Key.OemPlus }, |
|||
{"-", Key.OemMinus}, |
|||
{".", Key.OemPeriod } |
|||
}; |
|||
|
|||
//TODO: Move that to external key parser
|
|||
static Key ParseKey(string key) |
|||
{ |
|||
Key rv; |
|||
if (KeySynonims.TryGetValue(key.ToLower(), out rv)) |
|||
return rv; |
|||
return (Key)Enum.Parse(typeof (Key), key, true); |
|||
} |
|||
|
|||
static InputModifiers ParseModifier(string modifier) |
|||
{ |
|||
if (modifier.Equals("ctrl", StringComparison.OrdinalIgnoreCase)) |
|||
return InputModifiers.Control; |
|||
return (InputModifiers) Enum.Parse(typeof (InputModifiers), modifier, true); |
|||
} |
|||
|
|||
public static KeyGesture Parse(string gesture) |
|||
{ |
|||
//string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture
|
|||
|
|||
var parts = new List<string>(); |
|||
|
|||
var cstart = 0; |
|||
for (var c = 0; c <= gesture.Length; c++) |
|||
{ |
|||
var ch = c == gesture.Length ? '\0' : gesture[c]; |
|||
if (c == gesture.Length || (ch == '+' && cstart != c)) |
|||
{ |
|||
parts.Add(gesture.Substring(cstart, c - cstart)); |
|||
cstart = c + 1; |
|||
} |
|||
} |
|||
for (var c = 0; c < parts.Count; c++) |
|||
parts[c] = parts[c].Trim(); |
|||
|
|||
var rv = new KeyGesture(); |
|||
|
|||
for (var c = 0; c < parts.Count; c++) |
|||
{ |
|||
if (c == parts.Count - 1) |
|||
rv.Key = ParseKey(parts[c]); |
|||
else |
|||
rv.Modifiers |= ParseModifier(parts[c]); |
|||
} |
|||
return rv; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
var parts = new List<string>(); |
|||
foreach (var flag in Enum.GetValues(typeof (InputModifiers)).Cast<InputModifiers>()) |
|||
{ |
|||
if (Modifiers.HasFlag(flag) && flag != InputModifiers.None) |
|||
parts.Add(flag.ToString()); |
|||
} |
|||
parts.Add(Key.ToString()); |
|||
return string.Join(" + ", parts); |
|||
} |
|||
|
|||
public bool Matches(KeyEventArgs keyEvent) => keyEvent.Key == Key && keyEvent.Modifiers == Modifiers; |
|||
} |
|||
} |
|||
@ -0,0 +1,332 @@ |
|||
// Copyright (c) The Perspex 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.Subjects; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Base.UnitTests |
|||
{ |
|||
public class PerspexObjectTests_Direct |
|||
{ |
|||
[Fact] |
|||
public void GetValue_Gets_Value() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
Assert.Equal("initial", target.GetValue(Class1.FooProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_Gets_Value_NonGeneric() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
Assert.Equal("initial", target.GetValue((PerspexProperty)Class1.FooProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_On_Unregistered_Property_Throws_Exception() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => target.GetValue(Class1.BarProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Sets_Value() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
target.SetValue(Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.Equal("newvalue", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Sets_Value_NonGeneric() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.Equal("newvalue", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Raises_PropertyChanged() |
|||
{ |
|||
var target = new Class1(); |
|||
bool raised = false; |
|||
|
|||
target.PropertyChanged += (s, e) => |
|||
raised = e.Property == Class1.FooProperty && |
|||
(string)e.OldValue == "initial" && |
|||
(string)e.NewValue == "newvalue" && |
|||
e.Priority == BindingPriority.LocalValue; |
|||
|
|||
target.SetValue(Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.True(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Raises_Changed() |
|||
{ |
|||
var target = new Class1(); |
|||
bool raised = false; |
|||
|
|||
Class1.FooProperty.Changed.Subscribe(e => |
|||
raised = e.Property == Class1.FooProperty && |
|||
(string)e.OldValue == "initial" && |
|||
(string)e.NewValue == "newvalue" && |
|||
e.Priority == BindingPriority.LocalValue); |
|||
|
|||
target.SetValue(Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.True(raised); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_On_Unregistered_Property_Throws_Exception() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => target.SetValue(Class1.BarProperty, "value")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetObservable_Returns_Values() |
|||
{ |
|||
var target = new Class1(); |
|||
List<string> values = new List<string>(); |
|||
|
|||
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x)); |
|||
target.Foo = "newvalue"; |
|||
|
|||
Assert.Equal(new[] { "initial", "newvalue" }, values); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Bind_Binds_Property_Value() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new Subject<string>(); |
|||
|
|||
var sub = target.Bind(Class1.FooProperty, source); |
|||
|
|||
Assert.Equal("initial", target.Foo); |
|||
source.OnNext("first"); |
|||
Assert.Equal("first", target.Foo); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Foo); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
source.OnNext("third"); |
|||
Assert.Equal("second", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Bind_Binds_Property_Value_NonGeneric() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new Subject<string>(); |
|||
|
|||
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); |
|||
|
|||
Assert.Equal("initial", target.Foo); |
|||
source.OnNext("first"); |
|||
Assert.Equal("first", target.Foo); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Foo); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
source.OnNext("third"); |
|||
Assert.Equal("second", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ReadOnly_Property_Cannot_Be_Set() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => |
|||
target.SetValue(Class1.BarProperty, "newvalue")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ReadOnly_Property_Cannot_Be_Set_NonGeneric() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => |
|||
target.SetValue((PerspexProperty)Class1.BarProperty, "newvalue")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ReadOnly_Property_Cannot_Be_Bound() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new Subject<string>(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => |
|||
target.Bind(Class1.BarProperty, source)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ReadOnly_Property_Cannot_Be_Bound_NonGeneric() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new Subject<string>(); |
|||
|
|||
Assert.Throws<ArgumentException>(() => |
|||
target.Bind(Class1.BarProperty, source)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_Gets_Value_On_AddOwnered_Property() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
Assert.Equal("initial2", target.GetValue(Class2.FooProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
Assert.Equal("initial2", target.GetValue(Class1.FooProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original_NonGeneric() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
Assert.Equal("initial2", target.GetValue((PerspexProperty)Class1.FooProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
target.SetValue(Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.Equal("newvalue", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original_NonGeneric() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue"); |
|||
|
|||
Assert.Equal("newvalue", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Bind_Binds_AddOwnered_Property_Value() |
|||
{ |
|||
var target = new Class2(); |
|||
var source = new Subject<string>(); |
|||
|
|||
var sub = target.Bind(Class1.FooProperty, source); |
|||
|
|||
Assert.Equal("initial2", target.Foo); |
|||
source.OnNext("first"); |
|||
Assert.Equal("first", target.Foo); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Foo); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
source.OnNext("third"); |
|||
Assert.Equal("second", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Bind_Binds_AddOwnered_Property_Value_NonGeneric() |
|||
{ |
|||
var target = new Class2(); |
|||
var source = new Subject<string>(); |
|||
|
|||
var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); |
|||
|
|||
Assert.Equal("initial2", target.Foo); |
|||
source.OnNext("first"); |
|||
Assert.Equal("first", target.Foo); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Foo); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
source.OnNext("third"); |
|||
Assert.Equal("second", target.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Property_Notifies_Initialized() |
|||
{ |
|||
Class1 target; |
|||
bool raised = false; |
|||
|
|||
Class1.FooProperty.Initialized.Subscribe(e => |
|||
raised = e.Property == Class1.FooProperty && |
|||
e.OldValue == PerspexProperty.UnsetValue && |
|||
(string)e.NewValue == "initial" && |
|||
e.Priority == BindingPriority.Unset); |
|||
|
|||
target = new Class1(); |
|||
|
|||
Assert.True(raised); |
|||
} |
|||
|
|||
private class Class1 : PerspexObject |
|||
{ |
|||
public static readonly PerspexProperty<string> FooProperty = |
|||
PerspexProperty.RegisterDirect<Class1, string>("Foo", o => o.Foo, (o, v) => o.Foo = v); |
|||
|
|||
public static readonly PerspexProperty<string> BarProperty = |
|||
PerspexProperty.RegisterDirect<Class1, string>("Bar", o => o.Bar); |
|||
|
|||
private string _foo = "initial"; |
|||
|
|||
private string _bar = "bar"; |
|||
|
|||
public string Foo |
|||
{ |
|||
get { return _foo; } |
|||
set { SetAndRaise(FooProperty, ref _foo, value); } |
|||
} |
|||
|
|||
public string Bar |
|||
{ |
|||
get { return _bar; } |
|||
} |
|||
} |
|||
|
|||
private class Class2 : PerspexObject |
|||
{ |
|||
public static readonly PerspexProperty<string> FooProperty = |
|||
Class1.FooProperty.AddOwner<Class2>(o => o.Foo, (o, v) => o.Foo = v); |
|||
|
|||
private string _foo = "initial2"; |
|||
|
|||
static Class2() |
|||
{ |
|||
} |
|||
|
|||
public string Foo |
|||
{ |
|||
get { return _foo; } |
|||
set { SetAndRaise(FooProperty, ref _foo, value); } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Perspex.Controls.Utils; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Controls.UnitTests.Utils |
|||
{ |
|||
public class AncestorFinderTests |
|||
{ |
|||
[Fact] |
|||
public void SanityCheck() |
|||
{ |
|||
var child = new Control(); |
|||
var parent = new Decorator(); |
|||
var grandParent = new Border(); |
|||
var grandParent2 = new Border(); |
|||
|
|||
IVisual currentParent = null; |
|||
var subscription = AncestorFinder.Create(child, typeof (Border)).Subscribe(s => currentParent = s); |
|||
|
|||
Assert.Null(currentParent); |
|||
parent.Child = child; |
|||
Assert.Null(currentParent); |
|||
grandParent.Child = parent; |
|||
Assert.Equal(grandParent, currentParent); |
|||
grandParent.Child = null; |
|||
grandParent2.Child = parent; |
|||
Assert.Equal(grandParent2, currentParent); |
|||
|
|||
subscription.Dispose(); |
|||
parent.Child = null; |
|||
Assert.Equal(grandParent2, currentParent); |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Moq; |
|||
using Perspex.Controls.Presenters; |
|||
using Perspex.Controls.Templates; |
|||
using Perspex.Input; |
|||
using Perspex.Platform; |
|||
using Perspex.Styling; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Controls.UnitTests.Utils |
|||
{ |
|||
public class HotKeyManagerTests |
|||
{ |
|||
[Fact] |
|||
public void HotKeyManager_Should_Register_And_Unregister_Key_Binding() |
|||
{ |
|||
using (PerspexLocator.EnterScope()) |
|||
{ |
|||
var windowImpl = new Mock<IWindowImpl>(); |
|||
var styler = new Mock<Styler>(); |
|||
|
|||
PerspexLocator.CurrentMutable |
|||
.Bind<IWindowImpl>().ToConstant(windowImpl.Object) |
|||
.Bind<IStyler>().ToConstant(styler.Object); |
|||
|
|||
var gesture1 = new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}; |
|||
var gesture2 = new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Control}; |
|||
|
|||
var tl = new Window(); |
|||
var button = new Button(); |
|||
tl.Content = button; |
|||
tl.Template = CreateWindowTemplate(); |
|||
tl.ApplyTemplate(); |
|||
|
|||
HotKeyManager.SetHotKey(button, gesture1); |
|||
|
|||
Assert.Equal(gesture1, tl.KeyBindings[0].Gesture); |
|||
|
|||
HotKeyManager.SetHotKey(button, gesture2); |
|||
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); |
|||
|
|||
tl.Content = null; |
|||
tl.Presenter.ApplyTemplate(); |
|||
|
|||
Assert.Empty(tl.KeyBindings); |
|||
|
|||
tl.Content = button; |
|||
tl.Presenter.ApplyTemplate(); |
|||
|
|||
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); |
|||
|
|||
HotKeyManager.SetHotKey(button, null); |
|||
Assert.Empty(tl.KeyBindings); |
|||
|
|||
} |
|||
} |
|||
|
|||
private ControlTemplate CreateWindowTemplate() |
|||
{ |
|||
return new ControlTemplate<Window>(parent => |
|||
{ |
|||
return new ContentPresenter |
|||
{ |
|||
Name = "contentPresenter", |
|||
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Input.UnitTests |
|||
{ |
|||
public class KeyGestureParseTests |
|||
{ |
|||
private static readonly Dictionary<string, KeyGesture> SampleData = new Dictionary<string, KeyGesture> |
|||
{ |
|||
{"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}}, |
|||
{" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} }, |
|||
{"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} } |
|||
}; |
|||
|
|||
|
|||
|
|||
[Fact] |
|||
public void Key_Gesture_Is_Able_To_Parse_Sample_Data() |
|||
{ |
|||
foreach (var d in SampleData) |
|||
Assert.Equal(d.Value, KeyGesture.Parse(d.Key)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionNodeBuilderTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Build_Single_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo")); |
|||
|
|||
AssertIsProperty(result[0], "Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Underscored_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("_Foo")); |
|||
|
|||
AssertIsProperty(result[0], "_Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsProperty(result[2], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Negated_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
Assert.IsType<LogicalNotNode>(result[0]); |
|||
AssertIsProperty(result[1], "Foo"); |
|||
AssertIsProperty(result[2], "Bar"); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Double_Negated_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(5, result.Count); |
|||
Assert.IsType<LogicalNotNode>(result[0]); |
|||
Assert.IsType<LogicalNotNode>(result[1]); |
|||
AssertIsProperty(result[2], "Foo"); |
|||
AssertIsProperty(result[3], "Bar"); |
|||
AssertIsProperty(result[4], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Indexed_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15); |
|||
Assert.IsType<IndexerNode>(result[1]); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Multiple_Indexed_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15, 6); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Multiple_Indexed_Property_With_Space() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 5, 16); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Consecutive_Indexers() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]")); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15); |
|||
AssertIsIndexer(result[2], 16); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Indexed_Property_In_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz")); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsIndexer(result[2], 5, 6); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
private void AssertIsProperty(ExpressionNode node, string name) |
|||
{ |
|||
Assert.IsType<PropertyAccessorNode>(node); |
|||
|
|||
var p = (PropertyAccessorNode)node; |
|||
Assert.Equal(name, p.PropertyName); |
|||
} |
|||
|
|||
private void AssertIsIndexer(ExpressionNode node, params object[] args) |
|||
{ |
|||
Assert.IsType<IndexerNode>(node); |
|||
|
|||
var e = (IndexerNode)node; |
|||
Assert.Equal(e.Arguments.ToArray(), args.ToArray()); |
|||
} |
|||
|
|||
private List<ExpressionNode> ToList(ExpressionNode node) |
|||
{ |
|||
var result = new List<ExpressionNode>(); |
|||
|
|||
while (node != null) |
|||
{ |
|||
result.Add(node); |
|||
node = node.Next; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionNodeBuilderTests_Errors |
|||
{ |
|||
[Fact] |
|||
public void Identifier_Cannot_Start_With_Digit() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("1Foo")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Identifier_Cannot_Start_With_Symbol() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.%Bar")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_End_With_Period() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar.")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Empty_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_In_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Digit_After_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Letter_After_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
// Copyright (c) The Perspex 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.Collections.ObjectModel; |
|||
using System.Reactive.Linq; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Indexer |
|||
{ |
|||
[Fact] |
|||
public async void Should_Get_Array_Value() |
|||
{ |
|||
var data = new { Foo = new [] { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("bar", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Get_MultiDimensional_Array_Value() |
|||
{ |
|||
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; |
|||
var target = new ExpressionObserver(data, "Foo[1, 1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("qux", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Get_List_Value() |
|||
{ |
|||
var data = new { Foo = new List<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("bar", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Add() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[2]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Add("baz"); |
|||
|
|||
Assert.Equal(new[] { null, "baz" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Remove() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[0]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.RemoveAt(0); |
|||
|
|||
Assert.Equal(new[] { "foo", "bar" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Replace() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo[1] = "baz"; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Move() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Move(0, 1); |
|||
|
|||
Assert.Equal(new[] { "bar", "foo" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Reset() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Clear(); |
|||
|
|||
Assert.Equal(new[] { "bar", null }, result); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
// Copyright (c) The Perspex 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 Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Negation |
|||
{ |
|||
[Fact] |
|||
public async void Should_Negate_Boolean_Value() |
|||
{ |
|||
var data = new { Foo = true }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_0() |
|||
{ |
|||
var data = new { Foo = 0 }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(true, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_1() |
|||
{ |
|||
var data = new { Foo = 1 }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_False_String() |
|||
{ |
|||
var data = new { Foo = "false" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(true, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_True_String() |
|||
{ |
|||
var data = new { Foo = "True" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Return_Empty_For_String_Not_Convertible_To_Boolean() |
|||
{ |
|||
var data = new { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Return_Empty_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.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Throw() |
|||
{ |
|||
var data = new { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => target.SetValue("bar")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
// Copyright (c) The Perspex 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.Reactive.Subjects; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Observable |
|||
{ |
|||
[Fact] |
|||
public void Should_Get_Simple_Observable_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var source = new BehaviorSubject<string>("foo"); |
|||
var data = new { Foo = source }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
source.OnNext("bar"); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new[] { null, "foo", "bar" }, result); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Property_Value_From_Observable() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var data = new Class1(); |
|||
var target = new ExpressionObserver(data, "Next.Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Next.OnNext(new Class2("foo")); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new[] { null, "foo" }, result); |
|||
|
|||
sub.Dispose(); |
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
} |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
public Subject<Class2> Next { get; } = new Subject<Class2>(); |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase |
|||
{ |
|||
public Class2(string foo) |
|||
{ |
|||
Foo = foo; |
|||
} |
|||
|
|||
public string Foo { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,242 @@ |
|||
// Copyright (c) The Perspex 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 Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Property |
|||
{ |
|||
[Fact] |
|||
public async void Should_Get_Simple_Property_Value() |
|||
{ |
|||
var data = new { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("foo", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Get_Simple_Property_Chain() |
|||
{ |
|||
var data = new { Foo = new { Bar = new { Baz = "baz" } } }; |
|||
var target = new ExpressionObserver(data, "Foo.Bar.Baz"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("baz", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Not_Have_Value_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.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Simple_Property_Value() |
|||
{ |
|||
var data = new Class1 { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo = "bar"; |
|||
|
|||
Assert.Equal(new[] { "foo", "bar" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_End_Of_Property_Chain_Changing() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
((Class2)data.Next).Bar = "baz"; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Changing() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
data.Next = null; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", null, "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
var breaking = new WithoutBar(); |
|||
data.Next = breaking; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", null, "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, breaking.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Set_Simple_Property_Value() |
|||
{ |
|||
var data = new Class1 { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
|
|||
Assert.True(target.SetValue("bar")); |
|||
Assert.Equal("bar", data.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Set_Property_At_The_End_Of_Chain() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.True(target.SetValue("baz")); |
|||
Assert.Equal("baz", ((Class2)data.Next).Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Return_False_For_Missing_Property() |
|||
{ |
|||
var data = new Class1 { Next = new WithoutBar()}; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.False(target.SetValue("baz")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Return_False_For_Missing_Object() |
|||
{ |
|||
var data = new Class1(); |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.False(target.SetValue("baz")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Throw_For_Wrong_Type() |
|||
{ |
|||
var data = new Class1 { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
|
|||
Assert.Throws<ArgumentException>(() => target.SetValue(1.2)); |
|||
} |
|||
|
|||
private interface INext |
|||
{ |
|||
int SubscriptionCount { get; } |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
private string _foo; |
|||
private INext _next; |
|||
|
|||
public string Foo |
|||
{ |
|||
get { return _foo; } |
|||
set |
|||
{ |
|||
_foo = value; |
|||
RaisePropertyChanged(nameof(Foo)); |
|||
} |
|||
} |
|||
|
|||
public INext Next |
|||
{ |
|||
get { return _next; } |
|||
set |
|||
{ |
|||
_next = value; |
|||
RaisePropertyChanged(nameof(Next)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase, INext |
|||
{ |
|||
private string _bar; |
|||
|
|||
public string Bar |
|||
{ |
|||
get { return _bar; } |
|||
set |
|||
{ |
|||
_bar = value; |
|||
RaisePropertyChanged(nameof(Bar)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class WithoutBar : NotifyingBase, INext |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// Copyright (c) The Perspex 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; |
|||
using System.Threading.Tasks; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Task |
|||
{ |
|||
[Fact] |
|||
public void Should_Get_Simple_Task_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var tcs = new TaskCompletionSource<string>(); |
|||
var data = new { Foo = tcs.Task }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
tcs.SetResult("foo"); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new object[] { null, "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Completed_Task_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var data = new { Foo = Task.FromResult("foo") }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
|
|||
Assert.Equal(new object[] { "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Property_Value_From_Task() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var tcs = new TaskCompletionSource<Class2>(); |
|||
var data = new Class1(tcs.Task); |
|||
var target = new ExpressionObserver(data, "Next.Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
tcs.SetResult(new Class2("foo")); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new object[] { null, "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
public Class1(Task<Class2> next) |
|||
{ |
|||
Next = next; |
|||
} |
|||
|
|||
public Task<Class2> Next { get; } |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase |
|||
{ |
|||
public Class2(string foo) |
|||
{ |
|||
Foo = foo; |
|||
} |
|||
|
|||
public string Foo { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class NotifyingBase : INotifyPropertyChanged |
|||
{ |
|||
private PropertyChangedEventHandler _propertyChanged; |
|||
|
|||
public event PropertyChangedEventHandler PropertyChanged |
|||
{ |
|||
add |
|||
{ |
|||
_propertyChanged += value; |
|||
++SubscriptionCount; |
|||
} |
|||
|
|||
remove |
|||
{ |
|||
if (_propertyChanged?.GetInvocationList().Contains(value) == true) |
|||
{ |
|||
_propertyChanged -= value; |
|||
--SubscriptionCount; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int SubscriptionCount |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
protected void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<Import Project="..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" /> |
|||
<Import Project="..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" /> |
|||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> |
|||
<PropertyGroup> |
|||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
|||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> |
|||
<ProjectGuid>{8EF392D5-1416-45AA-9956-7CBBC3229E8A}</ProjectGuid> |
|||
<OutputType>Library</OutputType> |
|||
<AppDesignerFolder>Properties</AppDesignerFolder> |
|||
<RootNamespace>Perspex.Markup.UnitTests</RootNamespace> |
|||
<AssemblyName>Perspex.Markup.UnitTests</AssemblyName> |
|||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion> |
|||
<FileAlignment>512</FileAlignment> |
|||
<NuGetPackageImportStamp> |
|||
</NuGetPackageImportStamp> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> |
|||
<DebugSymbols>true</DebugSymbols> |
|||
<DebugType>full</DebugType> |
|||
<Optimize>false</Optimize> |
|||
<OutputPath>bin\Debug\</OutputPath> |
|||
<DefineConstants>DEBUG;TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> |
|||
<DebugType>pdbonly</DebugType> |
|||
<Optimize>true</Optimize> |
|||
<OutputPath>bin\Release\</OutputPath> |
|||
<DefineConstants>TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Reference Include="System" /> |
|||
<Reference Include="System.Core" /> |
|||
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Xml.Linq" /> |
|||
<Reference Include="System.Data.DataSetExtensions" /> |
|||
<Reference Include="Microsoft.CSharp" /> |
|||
<Reference Include="System.Data" /> |
|||
<Reference Include="System.Net.Http" /> |
|||
<Reference Include="System.Xml" /> |
|||
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="xunit.assert, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="xunit.core, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Compile Include="Binding\ExpressionNodeBuilderTests_Errors.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Observable.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Task.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Indexer.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Negation.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Property.cs" /> |
|||
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" /> |
|||
<Compile Include="Binding\NotifyingBase.cs" /> |
|||
<Compile Include="Properties\AssemblyInfo.cs" /> |
|||
<Compile Include="UnitTestSynchronizationContext.cs" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Include="packages.config" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj"> |
|||
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project> |
|||
<Name>Perspex.Markup</Name> |
|||
</ProjectReference> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> |
|||
</ItemGroup> |
|||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> |
|||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> |
|||
<PropertyGroup> |
|||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> |
|||
</PropertyGroup> |
|||
<Error Condition="!Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props'))" /> |
|||
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props'))" /> |
|||
</Target> |
|||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. |
|||
Other similar extension points exist, see Microsoft.Common.targets. |
|||
<Target Name="BeforeBuild"> |
|||
</Target> |
|||
<Target Name="AfterBuild"> |
|||
</Target> |
|||
--> |
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
// General Information about an assembly is controlled through the following
|
|||
// set of attributes. Change these attribute values to modify the information
|
|||
// associated with an assembly.
|
|||
[assembly: AssemblyTitle("Perspex.Markup.UnitTests")] |
|||
[assembly: AssemblyDescription("")] |
|||
[assembly: AssemblyConfiguration("")] |
|||
[assembly: AssemblyCompany("")] |
|||
[assembly: AssemblyProduct("Perspex.Markup.UnitTests")] |
|||
[assembly: AssemblyCopyright("Copyright © 2015")] |
|||
[assembly: AssemblyTrademark("")] |
|||
[assembly: AssemblyCulture("")] |
|||
|
|||
// Setting ComVisible to false makes the types in this assembly not visible
|
|||
// to COM components. If you need to access a type in this assembly from
|
|||
// COM, set the ComVisible attribute to true on that type.
|
|||
[assembly: ComVisible(false)] |
|||
|
|||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|||
[assembly: Guid("8ef392d5-1416-45aa-9956-7cbbc3229e8a")] |
|||
|
|||
// Version information for an assembly consists of the following four values:
|
|||
//
|
|||
// Major Version
|
|||
// Minor Version
|
|||
// Build Number
|
|||
// Revision
|
|||
//
|
|||
// You can specify all the values or you can default the Build and Revision Numbers
|
|||
// by using the '*' as shown below:
|
|||
// [assembly: AssemblyVersion("1.0.*")]
|
|||
[assembly: AssemblyVersion("1.0.0.0")] |
|||
[assembly: AssemblyFileVersion("1.0.0.0")] |
|||
@ -0,0 +1,68 @@ |
|||
// Copyright (c) The Perspex 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.Disposables; |
|||
using System.Threading; |
|||
|
|||
namespace Perspex.Markup.UnitTests |
|||
{ |
|||
internal sealed class UnitTestSynchronizationContext : SynchronizationContext |
|||
{ |
|||
readonly List<Tuple<SendOrPostCallback, object>> _postedCallbacks = |
|||
new List<Tuple<SendOrPostCallback, object>>(); |
|||
|
|||
public static Scope Begin() |
|||
{ |
|||
var sync = new UnitTestSynchronizationContext(); |
|||
var old = SynchronizationContext.Current; |
|||
SynchronizationContext.SetSynchronizationContext(sync); |
|||
return new Scope(old, sync); |
|||
} |
|||
|
|||
public override void Send(SendOrPostCallback d, object state) |
|||
{ |
|||
d(state); |
|||
} |
|||
|
|||
public override void Post(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_postedCallbacks) |
|||
{ |
|||
_postedCallbacks.Add(Tuple.Create(d, state)); |
|||
} |
|||
} |
|||
|
|||
public void ExecutePostedCallbacks() |
|||
{ |
|||
lock (_postedCallbacks) |
|||
{ |
|||
_postedCallbacks.ForEach(t => t.Item1(t.Item2)); |
|||
_postedCallbacks.Clear(); |
|||
} |
|||
} |
|||
|
|||
public class Scope : IDisposable |
|||
{ |
|||
private SynchronizationContext _old; |
|||
private UnitTestSynchronizationContext _new; |
|||
|
|||
public Scope(SynchronizationContext old, UnitTestSynchronizationContext n) |
|||
{ |
|||
_old = old; |
|||
_new = n; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
SynchronizationContext.SetSynchronizationContext(_old); |
|||
} |
|||
|
|||
public void ExecutePostedCallbacks() |
|||
{ |
|||
_new.ExecutePostedCallbacks(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue