diff --git a/docs/spec/binding-from-code.md b/docs/spec/binding-from-code.md new file mode 100644 index 0000000000..76f3aa55c9 --- /dev/null +++ b/docs/spec/binding-from-code.md @@ -0,0 +1,156 @@ +# Binding from Code + +Avalonia binding from code works somewhat differently to WPF/UWP. At the low level, Avalonia's +binding system is based on Reactive Extensions' `IObservable` which is then built upon by XAML +bindings (which can also be instantiated in code). + +## Binding to an observable + +You can bind a property to an observable using the `AvaloniaObject.Bind` method: + +```csharp +// We use an Rx Subject here so we can push new values using OnNext +var source = new Subject(); +var textBlock = new TextBlock(); + +// Bind TextBlock.Text to source +textBlock.Bind(TextBlock.TextProperty, source); + +// Set textBlock.Text to "hello" +source.OnNext("hello"); +// Set textBlock.Text to "world!" +source.OnNext("world!"); +``` + +## Binding priorities + +You can also pass a priority to a binding. *Note: Priorities only apply to styled properties: they* +*are ignored for direct properties.* + +The priority is passed using the `BindingPriority` enum, which looks like this: + +```csharp +/// +/// The priority of a binding. +/// +public enum BindingPriority +{ + /// + /// A value that comes from an animation. + /// + Animation = -1, + + /// + /// A local value: this is the default. + /// + LocalValue = 0, + + /// + /// A triggered style binding. + /// + /// + /// A style trigger is a selector such as .class which overrides a + /// 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. + /// + StyleTrigger, + + /// + /// A binding to a property on the templated parent. + /// + TemplatedParent, + + /// + /// A style binding. + /// + Style, + + /// + /// The binding is uninitialized. + /// + Unset = int.MaxValue, +} +``` + +Bindings with a priority with a smaller number take precedence over bindings with a higher value +priority, and bindings added more recently take precedence over other bindings with the same +priority. Whenever the binding produces `AvaloniaProperty.UnsetValue` then the next binding in the +priority order is selected. + +## Setting a binding in an object initializer + +It is often useful to set up bindings in object initializers. You can do this using the indexer: + +```csharp +var source = new Subject(); +var textBlock = new TextBlock +{ + Foreground = Brushes.Red, + MaxWidth = 200, + [!TextBlock.TextProperty] = source.ToBinding(), +}; +``` + +Using this method you can also easily bind a property on one control to a property on another: + +```csharp +var textBlock1 = new TextBlock(); +var textBlock2 = new TextBlock +{ + Foreground = Brushes.Red, + MaxWidth = 200, + [!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty], +}; +``` + +Of course the indexer can be used outside object initializers too: + +```csharp +textBlock2[!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty]; +``` + +# Transforming binding values + +Because we're working with observables, we can easily transform the values we're binding! + +```csharp +var source = new Subject(); +var textBlock = new TextBlock +{ + Foreground = Brushes.Red, + MaxWidth = 200, + [!TextBlock.TextProperty] = source.Select(x => "Hello " + x).ToBinding(), +}; +``` + +# Using XAML bindings from code + +Sometimes when you want the additional features that XAML bindings provide, it's easier to use XAML bindings from code. For example, using only observables you could bind to a property on `DataContext` like this: + +```csharp +var textBlock = new TextBlock(); +var viewModelProperty = textBlock.GetObservable(TextBlock.DataContext) + .OfType() + .Select(x => x?.Name); +textBlock.Bind(TextBlock, viewModelProperty); +``` + +However, it might be preferable to use a XAML binding in this case: + +```csharp +var textBlock = new TextBlock +{ + [!TextBlock.TextProperty] = new Binding("Name") +}; +``` + +By using XAML binding objects, you get access to binding to named controls and [all the other features that XAML bindings bring](binding-from.xaml.md): + +```csharp +var textBlock = new TextBlock +{ + [!TextBlock.TextProperty] = new Binding("Text") { ElementName = "other" } +}; +``` + diff --git a/docs/spec/binding-from-xaml.md b/docs/spec/binding-from-xaml.md new file mode 100644 index 0000000000..143e3627c8 --- /dev/null +++ b/docs/spec/binding-from-xaml.md @@ -0,0 +1,99 @@ +# Binding from XAML + +Binding from XAML works on the whole the same as in other XAML frameworks: you use the `{Binding}` +markup extension. Avalonia does have some extra syntacic niceties however. Here's an overview of +what you can currently do in Avalonia: + +## Binding to a property on the DataContext + +By default a binding binds to a property on the `DataContext`, e.g.: + +```xml + + + + +``` + +An empty binding binds to DataContext itself + +```xml + + + + +``` + +This usage is identical to WPF/UWP etc. + +## Two way bindings and more + +You can also specify a binding `Mode`: + +```xml + + +``` + +This usage is identical to WPF/UWP etc. + +## Binding to a property on the templated parent + +When you're creating a control template and you want to bind to the templated parent you can use: + +```xml + + + +``` + +This usage is identical to WPF/UWP etc. + +## Binding to a named control + +If you want to bind to a property on another (named) control, you can use `ElementName` as in +WPF/UWP: + +```xml + + +``` + +However Avalonia also introduces a shorthand syntax for this: + +```xml + +``` + +## Negating bindings + +You can also negate the value of a binding using the `!` operator: + +```xml + +``` + +Here, the `TextBox` will only be enabled when the view model signals that it has no errors. Behind +the scenes, Avalonia tries to convert the incoming value to a boolean, and if it can be converted +it negates the value. If the incoming value cannot be converted to a boolean then no value will be +pushed to the binding target. + +This syntax is specific to Avalonia. + +## Binding to tasks and observables + +You can subscribe to the result of a task or an observable by using the `^` stream binding operator. + +```xml + + +``` + +This syntax is specific to Avalonia. + +*Note: the stream operator is actually extensible, see +[here](https://github.com/AvaloniaUI/Avalonia/blob/master/src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs) +for the interface to implement and [here](https://github.com/AvaloniaUI/Avalonia/blob/master/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs#L47) +for the registration.* diff --git a/docs/spec/toc.yml b/docs/spec/toc.yml index f225084c4e..b0981017e0 100644 --- a/docs/spec/toc.yml +++ b/docs/spec/toc.yml @@ -8,3 +8,7 @@ href: working-with-properties.md - name: Logging href: logging.md +- name: Binding from XAML + href: binding-from-xaml.md +- name: Binding from Code + href: binding-from-code.md diff --git a/docs/spec/working-with-properties.md b/docs/spec/working-with-properties.md index 74dd60a9b2..a8a383b733 100644 --- a/docs/spec/working-with-properties.md +++ b/docs/spec/working-with-properties.md @@ -71,6 +71,8 @@ property to the first: Console.WriteLine(textBlock2.Text); ``` +To read more about creating bindings from code, see [Binding from Code](binding-from-code.md). + # Subscribing to a Property on Any Object The `GetObservable` method returns an observable that tracks changes to a diff --git a/docs/tutorial/from-wpf.md b/docs/tutorial/from-wpf.md index f25de9cde4..f8bb3defa2 100644 --- a/docs/tutorial/from-wpf.md +++ b/docs/tutorial/from-wpf.md @@ -40,17 +40,6 @@ placed in a `DataTemplates` collection on each control (and on `Application`): -`ItemsControl`s don't currently have an `ItemTemplate` property: instead just -place the template for your items into the control's `DataTemplates`, e.g. - - - - - - - - - Data templates in Avalonia can also target interfaces and derived classes (which cannot be done in WPF) and so the order of `DataTemplate`s can be important: `DataTemplate`s within the same collection are evaluated in declaration order @@ -92,13 +81,8 @@ referred to using the `{StyleResource}` markup extension both inside and outside styles. For non-style-related resources, we suggest defining them in code and referring -to them in markup using the `{Static}` markup extension. There are [various -reasons](http://www.codemag.com/article/1501091) for this, but briefly: - -- Resources have to be parsed -- The tree has to be traversed to find them -- XAML doesn't handle immutable objects -- XAML syntax can be long-winded compared to C# +to them in markup using the `{Static}` markup extension. To read more about the reasoning for this, +see [this issue comment](https://github.com/AvaloniaUI/Avalonia/issues/462#issuecomment-191849723). ## Grid 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/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 3ca55529e6..685bf83a75 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -16,7 +16,13 @@ namespace Avalonia /// public static class AvaloniaObjectExtensions { - public static IBinding AsBinding(this IObservable source) + /// + /// Converts an to an . + /// + /// The type produced by the observable. + /// The observable + /// An . + public static IBinding ToBinding(this IObservable source) { return new BindingAdaptor(source.Select(x => (object)x)); } 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/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 74c7da20d6..3a2cb688cb 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters /// internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable { + private double _crossAxisOffset; + /// /// Initializes a new instance of the class. /// @@ -60,7 +62,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets a value indicating whether the items should be scroll horizontally or vertically. /// - public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; + public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical; /// /// Gets a value indicating whether logical scrolling is enabled. @@ -85,12 +87,28 @@ namespace Avalonia.Controls.Presenters /// /// Gets the as a . /// - public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); + public Size Extent + { + get + { + return Vertical ? + new Size(Owner.Panel.DesiredSize.Width, ExtentValue) : + new Size(ExtentValue, Owner.Panel.DesiredSize.Height); + } + } /// /// Gets the as a . /// - public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); + public Size Viewport + { + get + { + return Vertical ? + new Size(Owner.Panel.Bounds.Width, ViewportValue) : + new Size(ViewportValue, Owner.Panel.Bounds.Height); + } + } /// /// Gets or sets the as a . @@ -99,12 +117,28 @@ namespace Avalonia.Controls.Presenters { get { - return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); + return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset); } set { - OffsetValue = Vertical ? value.Y : value.X; + var oldCrossAxisOffset = _crossAxisOffset; + + if (Vertical) + { + OffsetValue = value.Y; + _crossAxisOffset = value.X; + } + else + { + OffsetValue = value.X; + _crossAxisOffset = value.Y; + } + + if (_crossAxisOffset != oldCrossAxisOffset) + { + Owner.InvalidateArrange(); + } } } @@ -143,6 +177,29 @@ namespace Avalonia.Controls.Presenters return result; } + /// + /// Carries out a measure for the related . + /// + /// The size available to the control. + /// The desired size for the control. + public virtual Size MeasureOverride(Size availableSize) + { + Owner.Panel.Measure(availableSize); + return Owner.Panel.DesiredSize; + } + + /// + /// Carries out an arrange for the related . + /// + /// The size available to the control. + /// The actual size used. + public virtual Size ArrangeOverride(Size finalSize) + { + var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset); + Owner.Panel.Arrange(new Rect(origin, finalSize)); + return finalSize; + } + /// public virtual void UpdateControls() { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index afa5bb76db..228ad65ffa 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -94,6 +94,44 @@ namespace Avalonia.Controls.Presenters } } + /// + public override Size MeasureOverride(Size availableSize) + { + var window = Owner.GetVisualRoot() as TopLevel; + + // If infinity is passed as the available size and we're virtualized then we need to + // fill the available space, but to do that we *don't* want to materialize all our + // items! Take a look at the root of the tree for a MaxClientSize and use that as + // the available size. + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + if (availableSize.Height == double.PositiveInfinity) + { + if (window != null) + { + availableSize = availableSize.WithHeight(window.PlatformImpl.MaxClientSize.Height); + } + } + + availableSize = availableSize.WithWidth(double.PositiveInfinity); + } + else + { + if (availableSize.Width == double.PositiveInfinity) + { + if (window != null) + { + availableSize = availableSize.WithWidth(window.PlatformImpl.MaxClientSize.Width); + } + } + + availableSize = availableSize.WithHeight(double.PositiveInfinity); + } + + Owner.Panel.Measure(availableSize); + return Owner.Panel.DesiredSize; + } + /// public override void UpdateControls() { @@ -481,9 +519,19 @@ namespace Avalonia.Controls.Presenters { layoutManager.ExecuteLayoutPass(); - if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + if (panel.ScrollDirection == Orientation.Vertical) { - OffsetValue += 1; + if (container.Bounds.Y < panel.Bounds.Y || container.Bounds.Bottom > panel.Bounds.Bottom) + { + OffsetValue += 1; + } + } + else + { + if (container.Bounds.X < panel.Bounds.X || container.Bounds.Right > panel.Bounds.Right) + { + OffsetValue += 1; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index fa15766e4d..185193f889 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -91,24 +91,15 @@ namespace Avalonia.Controls.Presenters _virtualizer?.ScrollIntoView(item); } + /// protected override Size MeasureOverride(Size availableSize) { - // If infinity is passed as the available size and we're virtualized then we need to - // fill the available space, but to do that we *don't* want to materialize all our - // items! Take a look at the root of the tree for a MaxClientSize and use that as - // the available size. - if (availableSize == Size.Infinity && VirtualizationMode != ItemVirtualizationMode.None) - { - var window = VisualRoot as TopLevel; - - if (window != null) - { - availableSize = window.PlatformImpl.MaxClientSize; - } - } + return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + } - Panel.Measure(availableSize); - return Panel.DesiredSize; + protected override Size ArrangeOverride(Size finalSize) + { + return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; } /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 6fd48ceed6..7a271e8615 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -240,7 +240,21 @@ namespace Avalonia.Controls protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - _presenter.ShowCaret(); + + // when navigating to a textbox via the tab key, select all text if + // 1) this textbox is *not* a multiline textbox + // 2) this textbox has any text to select + if (e.NavigationMethod == NavigationMethod.Tab && + !AcceptsReturn && + Text?.Length > 0) + { + SelectionStart = 0; + SelectionEnd = Text.Length; + } + else + { + _presenter.ShowCaret(); + } } protected override void OnLostFocus(RoutedEventArgs e) @@ -481,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; @@ -533,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) { @@ -624,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; } } } @@ -704,6 +718,10 @@ namespace Avalonia.Controls if (pos < text.Length) { --pos; + if (pos > 0 && Text[pos - 1] == '\r' && Text[pos] == '\n') + { + --pos; + } } break; @@ -806,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.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 3cde8bb49d..7cb74ebb33 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.Views }, }, [GridRepeater.TemplateProperty] = pt, - [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).AsBinding(), + [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(), } }; } @@ -64,7 +64,7 @@ namespace Avalonia.Diagnostics.Views TextWrapping = TextWrapping.NoWrap, [!ToolTip.TipProperty] = property .WhenAnyValue(x => x.Diagnostic) - .AsBinding(), + .ToBinding(), }; yield return new TextBlock @@ -73,13 +73,13 @@ namespace Avalonia.Diagnostics.Views [!TextBlock.TextProperty] = property .WhenAnyValue(v => v.Value) .Select(v => v?.ToString()) - .AsBinding(), + .ToBinding(), }; yield return new TextBlock { TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).AsBinding(), + [!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).ToBinding(), }; } } 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/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 85daff28b9..b7b83bf852 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -168,7 +168,7 @@ namespace Avalonia.Layout private void QueueLayoutPass() { - if (!_queued) + if (!_queued && !_running) { Dispatcher.UIThread.InvokeAsync(ExecuteLayoutPass, DispatcherPriority.Render); _queued = true; 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.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index c8436c376f..b12b2e3c31 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.NonValidatedProperty] = source.AsBinding(), + [!Class1.NonValidatedProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); @@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.ValidatedDirectProperty] = source.AsBinding(), + [!Class1.ValidatedDirectProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 99e44c8d2c..f8eea8c4eb 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -163,9 +163,9 @@ namespace Avalonia.Controls.UnitTests Content = new ItemsPresenter { Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).AsBinding(), - [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).AsBinding(), - [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).AsBinding(), + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), + [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), + [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(), } }); } @@ -187,7 +187,7 @@ namespace Avalonia.Controls.UnitTests new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).AsBinding(), + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 9999fa5346..c7992fe80f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -207,7 +207,7 @@ namespace Avalonia.Controls.UnitTests Content = new ItemsPresenter { Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).AsBinding(), + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), } }; } @@ -217,7 +217,7 @@ namespace Avalonia.Controls.UnitTests return new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).AsBinding(), + [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), }; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 02dffdead6..1433bc7f85 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Arrange(new Rect(0, 0, 100, 100)); var scroll = (ScrollContentPresenter)target.Parent; - Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(10, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); } @@ -212,8 +212,8 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); - Assert.Equal(new Size(0, 20), scroll.Extent); - Assert.Equal(new Size(0, 10), scroll.Viewport); + Assert.Equal(new Size(10, 20), scroll.Extent); + Assert.Equal(new Size(100, 10), scroll.Viewport); } [Fact] @@ -253,7 +253,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroll.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); - Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(10, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 2a59227b52..8d10b01f94 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); + Assert.Equal(new Size(100, 10), ((ILogicalScrollable)target).Viewport); } [Fact] @@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); + Assert.Equal(new Size(10, 100), ((ILogicalScrollable)target).Viewport); } [Fact] @@ -146,7 +146,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 95)); target.Arrange(new Rect(0, 0, 100, 95)); - Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); + Assert.Equal(new Size(100, 9), ((ILogicalScrollable)target).Viewport); } [Fact] @@ -772,6 +772,19 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Same(target.Panel.Children[0], result); } } + + [Fact] + public void Should_Return_Horizontal_Extent_And_Viewport() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(5, 100)); + target.Arrange(new Rect(0, 0, 5, 100)); + + Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent); + Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport); + } } public class Horizontal diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 960be2ce0e..3c2f2e4f5c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new ContentPresenter { - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).AsBinding(), + [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), } }; }), 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));