Browse Source

Merge branch 'master' into UpdateTravisCIConfig

pull/760/head
Steven Kirk 10 years ago
committed by GitHub
parent
commit
796cb3bc27
  1. 156
      docs/spec/binding-from-code.md
  2. 99
      docs/spec/binding-from-xaml.md
  3. 4
      docs/spec/toc.yml
  4. 2
      docs/spec/working-with-properties.md
  5. 20
      docs/tutorial/from-wpf.md
  6. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  7. 8
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  8. 57
      src/Avalonia.Base/Data/BindingChainException.cs
  9. 67
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  10. 52
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  11. 21
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  12. 36
      src/Avalonia.Controls/TextBox.cs
  13. 118
      src/Avalonia.Controls/Utils/StringUtils.cs
  14. 8
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  15. 25
      src/Avalonia.Input/AccessKeyHandler.cs
  16. 2
      src/Avalonia.Layout/LayoutManager.cs
  17. 2
      src/Markup/Avalonia.Markup.Xaml/OmniXAML
  18. 9
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  19. 57
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  20. 14
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  21. 42
      src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs
  22. 33
      src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs
  23. 10
      src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs
  24. 4
      src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs
  25. 18
      src/Markup/Avalonia.Markup/Data/Plugins/ObservableStreamPlugin.cs
  26. 4
      src/Markup/Avalonia.Markup/Data/Plugins/TaskStreamPlugin.cs
  27. 31
      src/Markup/Avalonia.Markup/Data/StreamNode.cs
  28. 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  29. 8
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  30. 4
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  31. 8
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  32. 19
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  33. 2
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  34. 9
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  35. 2
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  36. 51
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
  37. 4
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  38. 15
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs

156
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<string>();
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
/// <summary>
/// The priority of a binding.
/// </summary>
public enum BindingPriority
{
/// <summary>
/// A value that comes from an animation.
/// </summary>
Animation = -1,
/// <summary>
/// A local value: this is the default.
/// </summary>
LocalValue = 0,
/// <summary>
/// A triggered style binding.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// </remarks>
StyleTrigger,
/// <summary>
/// A binding to a property on the templated parent.
/// </summary>
TemplatedParent,
/// <summary>
/// A style binding.
/// </summary>
Style,
/// <summary>
/// The binding is uninitialized.
/// </summary>
Unset = int.MaxValue,
}
```
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<string>();
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<string>();
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<MyViewModel>()
.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" }
};
```

99
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
<!-- Binds to the tb.DataContext.Name property -->
<TextBlock Name="tb" Text="{Binding Name}"/>
<!-- Which is the same as ('Path' is optional) -->
<TextBlock Name="tb" Text="{Binding Path=Name}"/>
```
An empty binding binds to DataContext itself
```xml
<!-- Binds to the tb.DataContext property -->
<TextBlock Name="tb" Text="{Binding}"/>
<!-- Which is the same as -->
<TextBlock Name="tb" Text="{Binding .}"/>
```
This usage is identical to WPF/UWP etc.
## Two way bindings and more
You can also specify a binding `Mode`:
```xml
<!-- Bind two-way to the property (although this is actually the default binding mode for
TextBox.Text) so strictly speaking it's unnecessary here) -->
<TextBox Name="tb" Text="{Binding Name, Mode=TwoWay}"/>
```
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
<TextBlock Name="tb" Text="{TemplateBinding Caption}"/>
<!-- Which is short for -->
<TextBlock Name="tb" Text="{Binding Caption, RelativeSource={RelativeSource TemplatedParent}}"/>
```
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
<!-- Binds to the Tag property of a control named "other" -->
<TextBlock Text="{Binding Tag, ElementName=other}"/>
```
However Avalonia also introduces a shorthand syntax for this:
```xml
<TextBlock Text="{Binding #other.Tag}"/>
```
## Negating bindings
You can also negate the value of a binding using the `!` operator:
```xml
<TextBox IsEnabled="{Binding !HasErrors}"/>
```
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
<!-- If DataContext.Name is an IObservable<string> then this will bind to the length of each
string produced by the observable as each value is produced -->
<TextBlock Text="{Binding Name^.Length}"/>
```
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.*

4
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

2
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

20
docs/tutorial/from-wpf.md

@ -40,17 +40,6 @@ placed in a `DataTemplates` collection on each control (and on `Application`):
<ContentControl Content="{Binding Foo}"/>
<UserControl>
`ItemsControl`s don't currently have an `ItemTemplate` property: instead just
place the template for your items into the control's `DataTemplates`, e.g.
<ListBox Items="ItemsSource">
<ListBox.DataTemplates>
<DataTemplate>
<TextBlock Text="{Binding Caption}"/>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
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

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -44,7 +44,7 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingChainNullException.cs" />
<Compile Include="Data\BindingChainException.cs" />
<Compile Include="Data\BindingNotification.cs" />
<Compile Include="Data\IndexerBinding.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />

8
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -16,7 +16,13 @@ namespace Avalonia
/// </summary>
public static class AvaloniaObjectExtensions
{
public static IBinding AsBinding<T>(this IObservable<T> source)
/// <summary>
/// Converts an <see cref="IObservable{T}"/> to an <see cref="IBinding"/>.
/// </summary>
/// <typeparam name="T">The type produced by the observable.</typeparam>
/// <param name="source">The observable</param>
/// <returns>An <see cref="IBinding"/>.</returns>
public static IBinding ToBinding<T>(this IObservable<T> source)
{
return new BindingAdaptor(source.Select(x => (object)x));
}

57
src/Avalonia.Base/Data/BindingChainNullException.cs → 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.
/// </summary>
public class BindingChainNullException : Exception
public class BindingChainException : Exception
{
private string _message;
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// Initalizes a new instance of the <see cref="BindingChainException"/> class.
/// </summary>
public BindingChainNullException()
public BindingChainException()
{
}
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// Initalizes a new instance of the <see cref="BindingChainException"/> class.
/// </summary>
public BindingChainNullException(string message)
/// <param name="message">The error message.</param>
public BindingChainException(string message)
{
_message = message;
}
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// Initalizes a new instance of the <see cref="BindingChainException"/> class.
/// </summary>
/// <param name="message">The error message.</param>
/// <param name="expression">The expression.</param>
/// <param name="expressionNullPoint">
/// The point in the expression at which the null was encountered.
/// <param name="errorPoint">
/// The point in the expression at which the error was encountered.
/// </param>
public BindingChainNullException(string expression, string expressionNullPoint)
public BindingChainException(string message, string expression, string errorPoint)
{
_message = message;
Expression = expression;
ExpressionNullPoint = expressionNullPoint;
ExpressionErrorPoint = errorPoint;
}
/// <summary>
@ -48,37 +51,27 @@ namespace Avalonia.Data
public string Expression { get; protected set; }
/// <summary>
/// Gets the point in the expression at which the null was encountered.
/// Gets the point in the expression at which the error occured.
/// </summary>
public string ExpressionNullPoint { get; protected set; }
public string ExpressionErrorPoint { get; protected set; }
/// <inheritdoc/>
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.";
}
}
}

67
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary>
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
{
private double _crossAxisOffset;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
/// </summary>
@ -60,7 +62,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
/// </summary>
public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical;
public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical;
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled.
@ -85,12 +87,28 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
/// </summary>
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);
}
}
/// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary>
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);
}
}
/// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
@ -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;
}
/// <summary>
/// Carries out a measure for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="availableSize">The size available to the control.</param>
/// <returns>The desired size for the control.</returns>
public virtual Size MeasureOverride(Size availableSize)
{
Owner.Panel.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <summary>
/// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="finalSize">The size available to the control.</param>
/// <returns>The actual size used.</returns>
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;
}
/// <inheritdoc/>
public virtual void UpdateControls()
{

52
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -94,6 +94,44 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
}
}

21
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -91,24 +91,15 @@ namespace Avalonia.Controls.Presenters
_virtualizer?.ScrollIntoView(item);
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>

36
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<Exception>)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<UndoRedoState>.IUndoRedoHost.UndoRedoState

118
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;

8
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(),
};
}
}

25
src/Avalonia.Input/AccessKeyHandler.cs

@ -43,6 +43,16 @@ namespace Avalonia.Input
/// </summary>
private bool _ignoreAltUp;
/// <summary>
/// Whether the AltKey is down.
/// </summary>
private bool _altIsDown;
/// <summary>
/// Element to restore folowing AltKey taking focus.
/// </summary>
private IInputElement _restoreFocusElement;
/// <summary>
/// Gets or sets the window's main menu.
/// </summary>
@ -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;
}
}
/// <summary>
@ -179,6 +202,8 @@ namespace Avalonia.Input
switch (e.Key)
{
case Key.LeftAlt:
_altIsDown = false;
if (_ignoreAltUp)
{
_ignoreAltUp = false;

2
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;

2
src/Markup/Avalonia.Markup.Xaml/OmniXAML

@ -1 +1 @@
Subproject commit b122549406107170bbe6e67c0d6a1a4252beef77
Subproject commit 544af79d218127b4174da4be19896c5ca78eaa5d

9
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -42,7 +42,8 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\MarkupBindingChainNullException.cs" />
<Compile Include="Data\StreamNode.cs" />
<Compile Include="Data\MarkupBindingChainException.cs" />
<Compile Include="Data\CommonPropertyNames.cs" />
<Compile Include="Data\EmptyExpressionNode.cs" />
<Compile Include="Data\ExpressionNodeBuilder.cs" />
@ -63,9 +64,9 @@
<Compile Include="Data\Parsers\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\ObservableValuePlugin.cs" />
<Compile Include="Data\Plugins\TaskValuePlugin.cs" />
<Compile Include="Data\Plugins\IValuePlugin.cs" />
<Compile Include="Data\Plugins\ObservableStreamPlugin.cs" />
<Compile Include="Data\Plugins\TaskStreamPlugin.cs" />
<Compile Include="Data\Plugins\IStreamPlugin.cs" />
<Compile Include="Data\Plugins\PropertyAccessorBase.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\Plugins\DataValidatiorBase.cs" />

57
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Markup.Data
private WeakReference _target = UnsetReference;
private IDisposable _valueSubscription;
private IObserver<object> _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);
}

14
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -41,14 +41,14 @@ namespace Avalonia.Markup.Data
};
/// <summary>
/// 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.
/// </summary>
public static readonly IList<IValuePlugin> ValueHandlers =
new List<IValuePlugin>
public static readonly IList<IStreamPlugin> StreamHandlers =
new List<IStreamPlugin>
{
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)
{

42
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<string> _nodes = new List<string>();
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;
}
}
}

33
src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs

@ -1,33 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal class MarkupBindingChainNullException : BindingChainNullException
{
private IList<string> _nodes = new List<string>();
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;
}
}
}

10
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,

4
src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs

@ -6,9 +6,9 @@ using System;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines how values are observed by an <see cref="ExpressionObserver"/>.
/// Defines a plugin that handles the '^' stream binding operator.
/// </summary>
public interface IValuePlugin
public interface IStreamPlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.

18
src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs → 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
{
/// <summary>
/// Handles binding to <see cref="IObservable{T}"/>s in an <see cref="ExpressionObserver"/>.
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
/// </summary>
public class ObservableValuePlugin : IValuePlugin
public class ObservableStreamPlugin : IStreamPlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
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<object> && !(target is ICommand);
}
public virtual bool Match(WeakReference reference) => reference.Target is IObservable<object>;
/// <summary>
/// Starts producing output based on the specified value.

4
src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/TaskStreamPlugin.cs

@ -12,9 +12,9 @@ using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Handles binding to <see cref="Task"/>s in an <see cref="ExpressionObserver"/>.
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
/// </summary>
public class TaskValuePlugin : IValuePlugin
public class TaskStreamPlugin : IStreamPlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.

31
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<object> 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));
}
}
}

4
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests
var source = new Subject<object>();
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<object>();
var target = new Class1
{
[!Class1.ValidatedDirectProperty] = source.AsBinding(),
[!Class1.ValidatedDirectProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));

8
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],

4
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(),
};
}

8
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;

19
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

2
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(),
}
};
}),

9
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);

2
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);

51
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<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -59,7 +77,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo", true);
var target = new ExpressionObserver(data, "Foo^", true);
var result = new List<object>();
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<object>();
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<object>();
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<Class2> Next { get; } = new Subject<Class2>();

4
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"

15
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<Task<string>>(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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -54,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var tcs = new TaskCompletionSource<Class2>();
var data = new Class1(tcs.Task);
var target = new ExpressionObserver(data, "Next.Foo");
var target = new ExpressionObserver(data, "Next^.Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -72,7 +73,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo");
var target = new ExpressionObserver(data, "Foo^");
var result = new List<object>();
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -119,7 +120,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo", true);
var target = new ExpressionObserver(data, "Foo^", true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));

Loading…
Cancel
Save