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..7a271e8615 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -495,10 +495,10 @@ namespace Avalonia.Controls case 2: if (!StringUtils.IsStartOfWord(text, index)) { - SelectionStart = StringUtils.PreviousWord(text, index, false); + SelectionStart = StringUtils.PreviousWord(text, index); } - SelectionEnd = StringUtils.NextWord(text, index, false); + SelectionEnd = StringUtils.NextWord(text, index); break; case 3: SelectionStart = 0; @@ -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) { @@ -638,11 +638,11 @@ namespace Avalonia.Controls { if (direction > 0) { - CaretIndex += StringUtils.NextWord(text, caretIndex, false) - caretIndex; + CaretIndex += StringUtils.NextWord(text, caretIndex) - caretIndex; } else { - CaretIndex += StringUtils.PreviousWord(text, caretIndex, false) - caretIndex; + CaretIndex += StringUtils.PreviousWord(text, caretIndex) - caretIndex; } } } @@ -718,6 +718,10 @@ namespace Avalonia.Controls if (pos < text.Length) { --pos; + if (pos > 0 && Text[pos - 1] == '\r' && Text[pos] == '\n') + { + --pos; + } } break; @@ -820,12 +824,6 @@ namespace Avalonia.Controls SelectionStart = CaretIndex; MoveHorizontal(1, modifiers); SelectionEnd = CaretIndex; - - string selection = GetSelection(); - if (selection != " " && selection.EndsWith(" ")) - { - SelectionEnd = CaretIndex - 1; - } } UndoRedoState UndoRedoHelper.IUndoRedoHost.UndoRedoState diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs index 8571d663f5..2304866a85 100644 --- a/src/Avalonia.Controls/Utils/StringUtils.cs +++ b/src/Avalonia.Controls/Utils/StringUtils.cs @@ -57,7 +57,7 @@ namespace Avalonia.Controls.Utils } } - public static int PreviousWord(string text, int cursor, bool gtkMode) + public static int PreviousWord(string text, int cursor) { int begin; int i; @@ -81,60 +81,21 @@ namespace Avalonia.Controls.Utils return (cr > 0) ? cr : 0; } - if (gtkMode) - { - CharClass cc = GetCharClass(text[cursor - 1]); - begin = lf + 1; - i = cursor; - - // skip over the word, punctuation, or run of whitespace - while (i > begin && GetCharClass(text[i - 1]) == cc) - { - i--; - } + CharClass cc = GetCharClass(text[cursor - 1]); + begin = lf + 1; + i = cursor; - // if the cursor was at whitespace, skip back a word too - if (cc == CharClass.CharClassWhitespace && i > begin) - { - cc = GetCharClass(text[i - 1]); - while (i > begin && GetCharClass(text[i - 1]) == cc) - { - i--; - } - } - } - else + // skip over the word, punctuation, or run of whitespace + while (i > begin && GetCharClass(text[i - 1]) == cc) { - begin = lf + 1; - i = cursor; - - if (cursor < text.Length) - { - // skip to the beginning of this word - while (i > begin && !char.IsWhiteSpace(text[i - 1])) - { - i--; - } - - if (i < cursor && IsStartOfWord(text, i)) - { - return i; - } - } - - // skip to the start of the lwsp - while (i > begin && char.IsWhiteSpace(text[i - 1])) - { - i--; - } - - if (i > begin) - { - i--; - } + i--; + } - // skip to the beginning of the word - while (i > begin && !IsStartOfWord(text, i)) + // if the cursor was at whitespace, skip back a word too + if (cc == CharClass.CharClassWhitespace && i > begin) + { + cc = GetCharClass(text[i - 1]); + while (i > begin && GetCharClass(text[i - 1]) == cc) { i--; } @@ -143,7 +104,7 @@ namespace Avalonia.Controls.Utils return i; } - public static int NextWord(string text, int cursor, bool gtkMode) + public static int NextWord(string text, int cursor) { int i, lf, cr; @@ -169,50 +130,19 @@ namespace Avalonia.Controls.Utils return cursor; } - if (gtkMode) - { - CharClass cc = GetCharClass(text[cursor]); - i = cursor; - - // skip over the word, punctuation, or run of whitespace - while (i < cr && GetCharClass(text[i]) == cc) - { - i++; - } + CharClass cc = GetCharClass(text[cursor]); + i = cursor; - // skip any whitespace after the word/punct - while (i < cr && char.IsWhiteSpace(text[i])) - { - i++; - } - } - else + // skip over the word, punctuation, or run of whitespace + while (i < cr && GetCharClass(text[i]) == cc) { - i = cursor; - - // skip any whitespace before the word - while (i < cr && char.IsWhiteSpace(text[i])) - { - i++; - } - - // skip to the end of the current word - while (i < cr && !char.IsWhiteSpace(text[i])) - { - i++; - } - - // skip any whitespace after the word - while (i < cr && char.IsWhiteSpace(text[i])) - { - i++; - } + i++; + } - // find the start of the next word - while (i < cr && !IsStartOfWord(text, i)) - { - i++; - } + // skip any whitespace after the word/punct + while (i < cr && char.IsWhiteSpace(text[i])) + { + i++; } return i; diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 7acbc109fc..7baa4103d7 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -43,6 +43,16 @@ namespace Avalonia.Input /// private bool _ignoreAltUp; + /// + /// Whether the AltKey is down. + /// + private bool _altIsDown; + + /// + /// Element to restore folowing AltKey taking focus. + /// + private IInputElement _restoreFocusElement; + /// /// Gets or sets the window's main menu. /// @@ -110,8 +120,14 @@ namespace Avalonia.Input { if (e.Key == Key.LeftAlt) { + _altIsDown = true; + 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; + // 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; @@ -121,11 +137,18 @@ namespace Avalonia.Input // 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. e.Handled = true; } + else if (_altIsDown) + { + _ignoreAltUp = true; + } } /// @@ -179,6 +202,8 @@ namespace Avalonia.Input switch (e.Key) { case Key.LeftAlt: + _altIsDown = false; + if (_ignoreAltUp) { _ignoreAltUp = false; 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 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 88c4a6ab18..1c3f453280 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -42,7 +42,8 @@ Properties\SharedAssemblyInfo.cs - + + @@ -63,9 +64,9 @@ - - - + + + diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index b0957c7187..93f20e4c77 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; }); @@ -92,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); } @@ -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,44 +137,22 @@ 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() { 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..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(); @@ -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/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/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 new file mode 100644 index 0000000000..ebcbfc9598 --- /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.StreamHandlers) + { + if (plugin.Match(reference)) + { + return plugin.Start(reference); + } + } + + // TODO: Improve error. + return Observable.Return(new BindingNotification( + new MarkupBindingChainException("Stream operator applied to unsupported type", Description), + BindingErrorType.Error)); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 851657ab6c..cff49bc32e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -175,24 +175,25 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); Assert.Equal("First Second Third ", textBox.Text); - // (First Second| Third ) - textBox.CaretIndex = 12; + // (First Second |Third ) + textBox.CaretIndex = 13; RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); Assert.Equal("First Second ", textBox.Text); // (First Sec|ond ) textBox.CaretIndex = 9; RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); - Assert.Equal("First Sec ", textBox.Text); + Assert.Equal("First Sec", textBox.Text); // (Fi[rs]t Sec ) textBox.SelectionStart = 2; textBox.SelectionEnd = 4; RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); - Assert.Equal("Fit Sec ", textBox.Text); + Assert.Equal("Fit Sec", textBox.Text); // (Fit Sec| ) + textBox.Text += " "; textBox.CaretIndex = 7; RaiseKeyEvent(textBox, Key.Delete, InputModifiers.Control); Assert.Equal("Fit Sec", textBox.Text); 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 3263aaace2..640d82fa19 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)); @@ -92,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" 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));