From 0ddf4caa95c125f31515f718d30afaa57d8d746c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Oct 2016 16:32:14 +0100 Subject: [PATCH 1/8] Added a "stream" binding expression operator. Which is now required when wanting to get the value produced by a Task/IObservable rather than the Task/IObservable itself. Fixes #711. --- .../Avalonia.Markup/Avalonia.Markup.csproj | 1 + .../Avalonia.Markup/Data/ExpressionNode.cs | 53 +++++-------------- .../Data/Parsers/ExpressionParser.cs | 10 ++++ src/Markup/Avalonia.Markup/Data/StreamNode.cs | 31 +++++++++++ .../ExpressionObserverTests_Observable.cs | 26 +++++++-- .../Data/ExpressionObserverTests_Task.cs | 15 +++--- 6 files changed, 84 insertions(+), 52 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Data/StreamNode.cs diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 88c4a6ab18..616891c0ea 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -42,6 +42,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index b0957c7187..9d590b0e19 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Markup.Data private WeakReference _target = UnsetReference; private IDisposable _valueSubscription; private IObserver _observer; - private IDisposable _valuePluginSubscription; public abstract string Description { get; } public ExpressionNode Next { get; set; } @@ -37,7 +36,6 @@ namespace Avalonia.Markup.Data { _valueSubscription?.Dispose(); _valueSubscription = null; - _valuePluginSubscription?.Dispose(); _target = value; if (running) @@ -63,8 +61,6 @@ namespace Avalonia.Markup.Data { _valueSubscription?.Dispose(); _valueSubscription = null; - _valuePluginSubscription?.Dispose(); - _valuePluginSubscription = null; nextSubscription?.Dispose(); _observer = null; }); @@ -115,25 +111,22 @@ namespace Avalonia.Markup.Data source = StartListeningCore(_target); } - return source.Subscribe(TargetValueChanged); + return source.Subscribe(ValueChanged); } - private void TargetValueChanged(object value) + private void ValueChanged(object value) { var notification = value as BindingNotification; if (notification == null) { - if (!HandleSpecialValue(value)) + if (Next != null) { - if (Next != null) - { - Next.Target = new WeakReference(value); - } - else - { - _observer.OnNext(value); - } + Next.Target = new WeakReference(value); + } + else + { + _observer.OnNext(value); } } else @@ -144,38 +137,16 @@ namespace Avalonia.Markup.Data } else if (notification.HasValue) { - if (!HandleSpecialValue(notification.Value)) + if (Next != null) { - if (Next != null) - { - Next.Target = new WeakReference(notification.Value); - } - else - { - _observer.OnNext(value); - } + Next.Target = new WeakReference(notification.Value); } - } - } - } - - private bool HandleSpecialValue(object value) - { - if (_valuePluginSubscription == null) - { - var reference = new WeakReference(value); - - foreach (var plugin in ExpressionObserver.ValueHandlers) - { - if (plugin.Match(reference)) + else { - _valuePluginSubscription = plugin.Start(reference)?.Subscribe(TargetValueChanged); - return true; + _observer.OnNext(value); } } } - - return false; } private BindingNotification TargetNullNotification() diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs index 6f9f2925bc..93ddf77376 100644 --- a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs @@ -87,6 +87,11 @@ namespace Avalonia.Markup.Data.Parsers { return State.BeforeMember; } + else if (ParseStreamOperator(r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } else { var args = ArgumentListParser.Parse(r, '[', ']'); @@ -161,6 +166,11 @@ namespace Avalonia.Markup.Data.Parsers return !r.End && r.TakeIf('('); } + private static bool ParseStreamOperator(Reader r) + { + return !r.End && r.TakeIf('^'); + } + private enum State { Start, diff --git a/src/Markup/Avalonia.Markup/Data/StreamNode.cs b/src/Markup/Avalonia.Markup/Data/StreamNode.cs new file mode 100644 index 0000000000..7513b4323f --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/StreamNode.cs @@ -0,0 +1,31 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using Avalonia.Data; +using System.Reactive.Linq; + +namespace Avalonia.Markup.Data +{ + internal class StreamNode : ExpressionNode + { + public override string Description => "^"; + + protected override IObservable StartListeningCore(WeakReference reference) + { + foreach (var plugin in ExpressionObserver.ValueHandlers) + { + if (plugin.Match(reference)) + { + return plugin.Start(reference); + } + } + + // TODO: Improve error. + return Observable.Return(new BindingNotification( + new InvalidCastException("Value could not be streamed."), + BindingErrorType.Error)); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index 3263aaace2..5e1a392f96 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Observable { [Fact] - public void Should_Get_Simple_Observable_Value() + public void Should_Not_Get_Observable_Value_Without_Modifier_Char() { using (var sync = UnitTestSynchronizationContext.Begin()) { @@ -28,6 +28,24 @@ namespace Avalonia.Markup.UnitTests.Data source.OnNext("bar"); sync.ExecutePostedCallbacks(); + Assert.Equal(new[] { source }, result); + } + } + + [Fact] + public void Should_Get_Simple_Observable_Value() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject("foo"); + var data = new { Foo = source }; + var target = new ExpressionObserver(data, "Foo^"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + source.OnNext("bar"); + sync.ExecutePostedCallbacks(); + Assert.Equal(new[] { "foo", "bar" }, result); } } @@ -38,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = new ExpressionObserver(data, "Next.Foo"); + var target = new ExpressionObserver(data, "Next^.Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -59,7 +77,7 @@ namespace Avalonia.Markup.UnitTests.Data { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(data, "Foo", true); + var target = new ExpressionObserver(data, "Foo^", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -78,7 +96,7 @@ namespace Avalonia.Markup.UnitTests.Data using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = new ExpressionObserver(data, "Next.Foo", true); + var target = new ExpressionObserver(data, "Next^.Foo", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index 3dcd8a4fbc..61e6dcb833 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Task { [Fact] - public void Should_Get_Simple_Task_Value() + public void Should_Not_Get_Task_Result_Without_Modifier_Char() { using (var sync = UnitTestSynchronizationContext.Begin()) { @@ -28,7 +28,8 @@ namespace Avalonia.Markup.UnitTests.Data tcs.SetResult("foo"); sync.ExecutePostedCallbacks(); - Assert.Equal(new[] { "foo" }, result); + Assert.Equal(1, result.Count); + Assert.IsType>(result[0]); } } @@ -38,7 +39,7 @@ namespace Avalonia.Markup.UnitTests.Data using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = new ExpressionObserver(data, "Foo"); + var target = new ExpressionObserver(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -54,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data { var tcs = new TaskCompletionSource(); var data = new Class1(tcs.Task); - var target = new ExpressionObserver(data, "Next.Foo"); + var target = new ExpressionObserver(data, "Next^.Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -72,7 +73,7 @@ namespace Avalonia.Markup.UnitTests.Data { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo"); + var target = new ExpressionObserver(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -96,7 +97,7 @@ namespace Avalonia.Markup.UnitTests.Data using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = TaskFromException(new NotSupportedException()) }; - var target = new ExpressionObserver(data, "Foo"); + var target = new ExpressionObserver(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -119,7 +120,7 @@ namespace Avalonia.Markup.UnitTests.Data { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo", true); + var target = new ExpressionObserver(data, "Foo^", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); From 715149b1f5b5b11696b00adbd0b4ed71abeb4f29 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 10 Oct 2016 22:35:44 +0200 Subject: [PATCH 2/8] Improve stream operator error message. When a stream operator is applied to an unsupported type. --- src/Avalonia.Base/Avalonia.Base.csproj | 2 +- ...lException.cs => BindingChainException.cs} | 57 ++++++++----------- src/Avalonia.Controls/TextBox.cs | 2 +- .../Avalonia.Markup/Avalonia.Markup.csproj | 2 +- .../Avalonia.Markup/Data/ExpressionNode.cs | 4 +- .../Data/ExpressionObserver.cs | 2 +- .../Data/MarkupBindingChainException.cs | 42 ++++++++++++++ .../Data/MarkupBindingChainNullException.cs | 33 ----------- src/Markup/Avalonia.Markup/Data/StreamNode.cs | 2 +- .../ExpressionObserverTests_DataValidation.cs | 2 +- .../ExpressionObserverTests_Observable.cs | 25 ++++++++ .../Data/ExpressionObserverTests_Property.cs | 4 +- 12 files changed, 102 insertions(+), 75 deletions(-) rename src/Avalonia.Base/Data/{BindingChainNullException.cs => BindingChainException.cs} (52%) create mode 100644 src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs delete mode 100644 src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index bc52e31d2c..63f500270d 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -44,7 +44,7 @@ Properties\SharedAssemblyInfo.cs - + diff --git a/src/Avalonia.Base/Data/BindingChainNullException.cs b/src/Avalonia.Base/Data/BindingChainException.cs similarity index 52% rename from src/Avalonia.Base/Data/BindingChainNullException.cs rename to src/Avalonia.Base/Data/BindingChainException.cs index 0e50a36d8a..97b0d3ba8b 100644 --- a/src/Avalonia.Base/Data/BindingChainNullException.cs +++ b/src/Avalonia.Base/Data/BindingChainException.cs @@ -10,36 +10,39 @@ namespace Avalonia.Data /// requested binding expression could not be evaluated because of a null in one of the links /// of the binding chain. /// - public class BindingChainNullException : Exception + public class BindingChainException : Exception { private string _message; /// - /// Initalizes a new instance of the class. + /// Initalizes a new instance of the class. /// - public BindingChainNullException() + public BindingChainException() { } /// - /// Initalizes a new instance of the class. + /// Initalizes a new instance of the class. /// - public BindingChainNullException(string message) + /// The error message. + public BindingChainException(string message) { _message = message; } /// - /// Initalizes a new instance of the class. + /// Initalizes a new instance of the class. /// + /// The error message. /// The expression. - /// - /// The point in the expression at which the null was encountered. + /// + /// The point in the expression at which the error was encountered. /// - public BindingChainNullException(string expression, string expressionNullPoint) + public BindingChainException(string message, string expression, string errorPoint) { + _message = message; Expression = expression; - ExpressionNullPoint = expressionNullPoint; + ExpressionErrorPoint = errorPoint; } /// @@ -48,37 +51,27 @@ namespace Avalonia.Data public string Expression { get; protected set; } /// - /// Gets the point in the expression at which the null was encountered. + /// Gets the point in the expression at which the error occured. /// - public string ExpressionNullPoint { get; protected set; } + public string ExpressionErrorPoint { get; protected set; } /// public override string Message { get { - if (_message == null) + if (Expression != null && ExpressionErrorPoint != null) { - _message = BuildMessage(); + return $"{_message} in expression '{Expression}' at '{ExpressionErrorPoint}'."; + } + else if (ExpressionErrorPoint != null) + { + return $"{_message} in expression '{ExpressionErrorPoint}'."; + } + else + { + return $"{_message} in expression."; } - - return _message; - } - } - - private string BuildMessage() - { - if (Expression != null && ExpressionNullPoint != null) - { - return $"'{ExpressionNullPoint}' is null in expression '{Expression}'."; - } - else if (ExpressionNullPoint != null) - { - return $"'{ExpressionNullPoint}' is null in expression."; - } - else - { - return "Null encountered in binding expression."; } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index ed92899cc6..bc8da38724 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -547,7 +547,7 @@ namespace Avalonia.Controls var exceptions = aggregate == null ? (IEnumerable)new[] { exception } : aggregate.InnerExceptions; - var filtered = exceptions.Where(x => !(x is BindingChainNullException)).ToList(); + var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList(); if (filtered.Count > 0) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 616891c0ea..d97ab89a09 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -43,7 +43,7 @@ Properties\SharedAssemblyInfo.cs - + diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 9d590b0e19..93f20e4c77 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -88,7 +88,7 @@ namespace Avalonia.Markup.Data protected virtual void NextValueChanged(object value) { - var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainNullException; + var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; bindingBroken?.AddNode(Description); _observer.OnNext(value); } @@ -152,7 +152,7 @@ namespace Avalonia.Markup.Data private BindingNotification TargetNullNotification() { return new BindingNotification( - new MarkupBindingChainNullException(), + new MarkupBindingChainException("Null value"), BindingErrorType.Error, AvaloniaProperty.UnsetValue); } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 819949b7b9..90c1aa2894 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -235,7 +235,7 @@ namespace Avalonia.Markup.Data } else { - var broken = BindingNotification.ExtractError(o) as MarkupBindingChainNullException; + var broken = BindingNotification.ExtractError(o) as MarkupBindingChainException; if (broken != null) { diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs new file mode 100644 index 0000000000..dab5756976 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + internal class MarkupBindingChainException : BindingChainException + { + private IList _nodes = new List(); + + public MarkupBindingChainException(string message) + : base(message) + { + } + + public MarkupBindingChainException(string message, string node) + : base(message) + { + AddNode(node); + } + + public MarkupBindingChainException(string message, string expression, string expressionNullPoint) + : base(message, expression, expressionNullPoint) + { + _nodes = null; + } + + public bool HasNodes => _nodes.Count > 0; + public void AddNode(string node) => _nodes.Add(node); + + public void Commit(string expression) + { + Expression = expression; + ExpressionErrorPoint = string.Join(".", _nodes.Reverse()) + .Replace(".!", "!") + .Replace(".[", "[") + .Replace(".^", "^"); + _nodes = null; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs deleted file mode 100644 index a549d6ebb6..0000000000 --- a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.Data; - -namespace Avalonia.Markup.Data -{ - internal class MarkupBindingChainNullException : BindingChainNullException - { - private IList _nodes = new List(); - - public MarkupBindingChainNullException() - { - } - - public MarkupBindingChainNullException(string expression, string expressionNullPoint) - : base(expression, expressionNullPoint) - { - _nodes = null; - } - - public bool HasNodes => _nodes.Count > 0; - public void AddNode(string node) => _nodes.Add(node); - - public void Commit(string expression) - { - Expression = expression; - ExpressionNullPoint = string.Join(".", _nodes.Reverse()) - .Replace(".!", "!") - .Replace(".[", "["); - _nodes = null; - } - } -} diff --git a/src/Markup/Avalonia.Markup/Data/StreamNode.cs b/src/Markup/Avalonia.Markup/Data/StreamNode.cs index 7513b4323f..7a5cfe5009 100644 --- a/src/Markup/Avalonia.Markup/Data/StreamNode.cs +++ b/src/Markup/Avalonia.Markup/Data/StreamNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Data // TODO: Improve error. return Observable.Return(new BindingNotification( - new InvalidCastException("Value could not be streamed."), + new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs index 546cfe015f..3b5ca26db1 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs @@ -143,7 +143,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { new BindingNotification( - new MarkupBindingChainNullException("Inner.MustBePositive", "Inner"), + new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index 5e1a392f96..640d82fa19 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -110,6 +110,31 @@ namespace Avalonia.Markup.UnitTests.Data } } + [Fact] + public void Should_Return_BindingNotification_If_Stream_Operator_Applied_To_Not_Supported_Type() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new Class2("foo"); + var target = new ExpressionObserver(data, "Foo^", true); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + sync.ExecutePostedCallbacks(); + + Assert.Equal( + new[] + { + new BindingNotification( + new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"), + BindingErrorType.Error) + }, + result); + + sub.Dispose(); + } + } + private class Class1 : NotifyingBase { public Subject Next { get; } = new Subject(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index aa9ee7d58b..bdcd39d997 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -146,7 +146,7 @@ namespace Avalonia.Markup.UnitTests.Data new[] { new BindingNotification( - new MarkupBindingChainNullException("Foo.Bar.Baz", "Foo"), + new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, @@ -274,7 +274,7 @@ namespace Avalonia.Markup.UnitTests.Data { "bar", new BindingNotification( - new MarkupBindingChainNullException("Next.Next.Bar", "Next.Next"), + new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), "bar" From b6c51b34effb17629e94df89f4b4b0a6902e0767 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 10 Oct 2016 22:48:21 +0200 Subject: [PATCH 3/8] Renamed ValueHandler -> StreamPlugin --- .../Avalonia.Markup/Avalonia.Markup.csproj | 6 +++--- .../Avalonia.Markup/Data/ExpressionObserver.cs | 12 ++++++------ .../{IValuePlugin.cs => IStreamPlugin.cs} | 4 ++-- ...aluePlugin.cs => ObservableStreamPlugin.cs} | 18 +++--------------- ...{TaskValuePlugin.cs => TaskStreamPlugin.cs} | 4 ++-- src/Markup/Avalonia.Markup/Data/StreamNode.cs | 2 +- 6 files changed, 17 insertions(+), 29 deletions(-) rename src/Markup/Avalonia.Markup/Data/Plugins/{IValuePlugin.cs => IStreamPlugin.cs} (89%) rename src/Markup/Avalonia.Markup/Data/Plugins/{ObservableValuePlugin.cs => ObservableStreamPlugin.cs} (62%) rename src/Markup/Avalonia.Markup/Data/Plugins/{TaskValuePlugin.cs => TaskStreamPlugin.cs} (95%) diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index d97ab89a09..1c3f453280 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -64,9 +64,9 @@ - - - + + + diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 90c1aa2894..37226ee74b 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -41,14 +41,14 @@ namespace Avalonia.Markup.Data }; /// - /// An ordered collection of value handlers that can be used to customize the handling - /// of certain values. + /// An ordered collection of stream plugins that can be used to customize the behavior + /// of the '^' stream binding operator. /// - public static readonly IList ValueHandlers = - new List + public static readonly IList StreamHandlers = + new List { - new TaskValuePlugin(), - new ObservableValuePlugin(), + new TaskStreamPlugin(), + new ObservableStreamPlugin(), }; private static readonly object UninitializedValue = new object(); diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs similarity index 89% rename from src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs rename to src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs index fb285c6d73..efb2e2d93a 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs @@ -6,9 +6,9 @@ using System; namespace Avalonia.Markup.Data.Plugins { /// - /// Defines how values are observed by an . + /// Defines a plugin that handles the '^' stream binding operator. /// - public interface IValuePlugin + public interface IStreamPlugin { /// /// Checks whether this plugin handles the specified value. diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableStreamPlugin.cs similarity index 62% rename from src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs rename to src/Markup/Avalonia.Markup/Data/Plugins/ObservableStreamPlugin.cs index a406fc55b9..a1da42d28f 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableStreamPlugin.cs @@ -2,32 +2,20 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows.Input; -using Avalonia.Data; namespace Avalonia.Markup.Data.Plugins { /// - /// Handles binding to s in an . + /// Handles binding to s for the '^' stream binding operator. /// - public class ObservableValuePlugin : IValuePlugin + public class ObservableStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) - { - var target = reference.Target; - - // ReactiveCommand is an IObservable but we want to bind to it, not its value. - return target is IObservable && !(target is ICommand); - } + public virtual bool Match(WeakReference reference) => reference.Target is IObservable; /// /// Starts producing output based on the specified value. diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/TaskStreamPlugin.cs similarity index 95% rename from src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs rename to src/Markup/Avalonia.Markup/Data/Plugins/TaskStreamPlugin.cs index b6fda67503..d2c8c1b064 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/TaskStreamPlugin.cs @@ -12,9 +12,9 @@ using Avalonia.Data; namespace Avalonia.Markup.Data.Plugins { /// - /// Handles binding to s in an . + /// Handles binding to s for the '^' stream binding operator. /// - public class TaskValuePlugin : IValuePlugin + public class TaskStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. diff --git a/src/Markup/Avalonia.Markup/Data/StreamNode.cs b/src/Markup/Avalonia.Markup/Data/StreamNode.cs index 7a5cfe5009..ebcbfc9598 100644 --- a/src/Markup/Avalonia.Markup/Data/StreamNode.cs +++ b/src/Markup/Avalonia.Markup/Data/StreamNode.cs @@ -14,7 +14,7 @@ namespace Avalonia.Markup.Data protected override IObservable StartListeningCore(WeakReference reference) { - foreach (var plugin in ExpressionObserver.ValueHandlers) + foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { From b0e0596500daa237db46a95355657bb3d75c874e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 11 Oct 2016 00:29:07 +0200 Subject: [PATCH 4/8] Updated OmniXAML to allow '^' in markup extension. --- src/Markup/Avalonia.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/OmniXAML b/src/Markup/Avalonia.Markup.Xaml/OmniXAML index b122549406..544af79d21 160000 --- a/src/Markup/Avalonia.Markup.Xaml/OmniXAML +++ b/src/Markup/Avalonia.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit b122549406107170bbe6e67c0d6a1a4252beef77 +Subproject commit 544af79d218127b4174da4be19896c5ca78eaa5d From 4c5c099ec31d8e848742d8eff0db41cd1819c58c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 11 Oct 2016 21:43:52 +0100 Subject: [PATCH 5/8] Dont focus menu then alt key down event is followed by another keydown event. --- src/Avalonia.Input/AccessKeyHandler.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 7acbc109fc..b23cdf550d 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -43,6 +43,11 @@ namespace Avalonia.Input /// private bool _ignoreAltUp; + /// + /// Whether the AltKey is down. + /// + private bool _altIsDown; + /// /// Gets or sets the window's main menu. /// @@ -110,6 +115,8 @@ namespace Avalonia.Input { if (e.Key == Key.LeftAlt) { + _altIsDown = true; + if (MainMenu == null || !MainMenu.IsOpen) { // When Alt is pressed without a main menu, or with a closed main menu, show @@ -126,6 +133,10 @@ namespace Avalonia.Input // We always handle the Alt key. e.Handled = true; } + else if(_altIsDown) + { + _ignoreAltUp = true; + } } /// @@ -179,6 +190,8 @@ namespace Avalonia.Input switch (e.Key) { case Key.LeftAlt: + _altIsDown = false; + if (_ignoreAltUp) { _ignoreAltUp = false; From 3fea4d34f18a72a51f7691a6be523b9a934b353f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 11 Oct 2016 21:48:36 +0100 Subject: [PATCH 6/8] Restore focus of input element after alt-key has stolen focus. --- src/Avalonia.Input/AccessKeyHandler.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index b23cdf550d..7b88fe18f3 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -48,6 +48,11 @@ namespace Avalonia.Input /// private bool _altIsDown; + /// + /// Element to restore folowing AltKey taking focus. + /// + private IInputElement _restoreFocusElement; + /// /// Gets or sets the window's main menu. /// @@ -119,15 +124,21 @@ namespace Avalonia.Input if (MainMenu == null || !MainMenu.IsOpen) { + // Save currently focused input element. + _restoreFocusElement = FocusManager.Instance.Current; + // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner.ShowAccessKeys = _showingAccessKeys = true; + _owner.ShowAccessKeys = _showingAccessKeys = true; } else { // If the Alt key is pressed and the main menu is open, close the main menu. CloseMenu(); _ignoreAltUp = true; + + _restoreFocusElement?.Focus(); + _restoreFocusElement = null; } // We always handle the Alt key. From 980a04b38f959927bbf496dca442d9d9015d6ba1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 11 Oct 2016 21:53:34 +0100 Subject: [PATCH 7/8] Whitespace changes. --- src/Avalonia.Input/AccessKeyHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 7b88fe18f3..06e8e8cea5 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -129,14 +129,14 @@ namespace Avalonia.Input // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner.ShowAccessKeys = _showingAccessKeys = true; + _owner.ShowAccessKeys = _showingAccessKeys = true; } else { // If the Alt key is pressed and the main menu is open, close the main menu. CloseMenu(); _ignoreAltUp = true; - + _restoreFocusElement?.Focus(); _restoreFocusElement = null; } @@ -144,7 +144,7 @@ namespace Avalonia.Input // We always handle the Alt key. e.Handled = true; } - else if(_altIsDown) + else if (_altIsDown) { _ignoreAltUp = true; } From c33a7da97b64165b2e3c077b7a776ea00c6335e7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 12 Oct 2016 21:20:52 +0100 Subject: [PATCH 8/8] Added todo comment for focus scopes. --- src/Avalonia.Input/AccessKeyHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 06e8e8cea5..7baa4103d7 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -124,8 +124,9 @@ namespace Avalonia.Input if (MainMenu == null || !MainMenu.IsOpen) { + // TODO: Use FocusScopes to store the current element and restore it when context menu is closed. // Save currently focused input element. - _restoreFocusElement = FocusManager.Instance.Current; + _restoreFocusElement = FocusManager.Instance.Current; // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File").