Browse Source

Merge branch 'master' into wpf-integration

Nikita Tsukanov 9 years ago
committed by GitHub
parent
commit
41ed04f369
  1. 3
      .gitignore
  2. 25
      build.cake
  3. 2
      build/Moq.props
  4. 6
      build/XUnit.props
  5. 2
      docs/tutorial/from-wpf.md
  6. 1
      samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
  7. 38
      src/Avalonia.Base/Data/BindingNotification.cs
  8. 36
      src/Avalonia.Controls/Button.cs
  9. 11
      src/Avalonia.Controls/Control.cs
  10. 2
      src/Avalonia.Controls/Menu.cs
  11. 11
      src/Avalonia.Controls/MenuItem.cs
  12. 38
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  13. 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  14. 40
      src/Avalonia.Controls/ToolTip.cs
  15. 22
      src/Avalonia.Controls/TreeView.cs
  16. 3
      src/Avalonia.Input/Avalonia.Input.csproj
  17. 5
      src/Avalonia.Input/FocusManager.cs
  18. 15
      src/Avalonia.Input/ICustomKeyboardNavigation.cs
  19. 27
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  20. 16
      src/Avalonia.Input/Navigation/DirectionalNavigation.cs
  21. 87
      src/Avalonia.Input/Navigation/TabNavigation.cs
  22. 97
      src/Avalonia.Layout/LayoutManager.cs
  23. 16
      src/Avalonia.Layout/Layoutable.cs
  24. 10
      src/Avalonia.Styling/LogicalTree/ILogical.cs
  25. 4
      src/Avalonia.Styling/Styling/Style.cs
  26. 2
      src/Gtk/Avalonia.Gtk3/Interop/GObject.cs
  27. 2
      src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs
  28. 3
      src/Gtk/Avalonia.Gtk3/SystemDialogs.cs
  29. 2
      src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs
  30. 4
      src/Markup/Avalonia.Markup/Data/BindingExpression.cs
  31. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  32. 2
      tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs
  33. 3
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  34. 65
      tests/Avalonia.Benchmarks/Layout/Measure.cs
  35. 1
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  36. 4
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  37. 291
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  38. 190
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs
  39. 102
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs
  40. 109
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  41. 28
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  42. 19
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  43. 44
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  44. 1
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  45. 214
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
  46. 1
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  47. 1
      tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj
  48. 272
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  49. 29
      tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs
  50. 43
      tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs
  51. 90
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
  52. 24
      tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs
  53. 1
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  54. 3
      tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs
  55. 115
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  56. 5
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs
  57. 3
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs
  58. 9
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  59. 67
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
  60. 5
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
  61. 31
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
  62. 12
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
  63. 78
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  64. 12
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
  65. 2
      tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
  66. 2
      tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs
  67. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  68. 3
      tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs
  69. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs
  70. 1
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  71. 8
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  72. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  73. 4
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  74. 15
      tests/Avalonia.UnitTests/InvariantCultureFixture.cs
  75. 2
      tests/Avalonia.UnitTests/TestRoot.cs
  76. 6
      tests/Avalonia.UnitTests/TestServices.cs
  77. 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs
  78. 4
      tools/packages.config

3
.gitignore

@ -162,7 +162,8 @@ $RECYCLE.BIN/
#################
## Cake
#################
tools/
tools/*
!tools/packages.config
.nuget
artifacts/
nuget

25
build.cake

@ -11,7 +11,7 @@
// TOOLS
///////////////////////////////////////////////////////////////////////////////
#tool "nuget:?package=xunit.runner.console&version=2.1.0"
#tool "nuget:?package=xunit.runner.console&version=2.2.0"
#tool "nuget:?package=OpenCover"
///////////////////////////////////////////////////////////////////////////////
@ -98,7 +98,6 @@ Task("Clean")
CleanDirectory(parameters.TestsRoot);
});
Task("Restore-NuGet-Packages")
.IsDependentOn("Clean")
.WithCriteria(parameters.IsRunningOnWindows)
@ -171,23 +170,25 @@ void RunCoreTest(string dir, Parameters parameters, bool net461Only)
continue;
Information("Running for " + fw);
DotNetCoreTest(System.IO.Path.Combine(dir, System.IO.Path.GetFileName(dir)+".csproj"),
new DotNetCoreTestSettings{Framework = fw});
new DotNetCoreTestSettings {
Configuration = parameters.Configuration,
Framework = fw
});
}
}
Task("Run-Net-Core-Unit-Tests")
.IsDependentOn("Clean")
.Does(() => {
RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, true);
//RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, true);
//RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, true);
RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false);
});
Task("Run-Unit-Tests")

2
build/Moq.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Moq" Version="4.7.1" />
<PackageReference Include="Moq" Version="4.7.25" />
</ItemGroup>
</Project>

6
build/XUnit.props

@ -7,7 +7,9 @@
<PackageReference Include="xunit.extensibility.core" Version="2.2.0" />
<PackageReference Include="xunit.extensibility.execution" Version="2.2.0" />
<PackageReference Include="xunit.runner.console" Version="2.2.0" />
<PackageReference Condition="'$(TargetFramework)' == 'net461'" Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Condition="'$(TargetFramework)' == 'netcoreapp1.1'" Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
</ItemGroup>
</Project>

2
docs/tutorial/from-wpf.md

@ -33,7 +33,7 @@ placed in a `DataTemplates` collection on each control (and on `Application`):
<TextBox Text="{Binding Name}"/>
</Border>
</DataTemplate>
</UserControl.Styles>
</UserControl.DataTemplates>
<!-- Assuming that DataContext.Foo is an object of type
MyApp.ViewModels.FooViewModel then a red border with a corner
radius of 8 containing a TextBox will be displayed here -->

1
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

@ -185,5 +185,6 @@
</ItemGroup>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="$(MSBuildThisFileDirectory)..\..\..\src\Shared\nuget.workaround.targets" />
</Project>

38
src/Avalonia.Base/Data/BindingNotification.cs

@ -44,11 +44,7 @@ namespace Avalonia.Data
public static readonly BindingNotification UnsetValue =
new BindingNotification(AvaloniaProperty.UnsetValue);
// Null cannot be held in WeakReference as it's indistinguishable from an expired value so
// use this value in its place.
private static readonly object NullValue = new object();
private WeakReference<object> _value;
private object _value;
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
@ -56,7 +52,7 @@ namespace Avalonia.Data
/// <param name="value">The binding value.</param>
public BindingNotification(object value)
{
_value = new WeakReference<object>(value ?? NullValue);
_value = value;
}
/// <summary>
@ -73,6 +69,7 @@ namespace Avalonia.Data
Error = error;
ErrorType = errorType;
_value = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -84,7 +81,7 @@ namespace Avalonia.Data
public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue)
: this(error, errorType)
{
_value = new WeakReference<object>(fallbackValue ?? NullValue);
_value = fallbackValue;
}
/// <summary>
@ -95,31 +92,12 @@ namespace Avalonia.Data
/// If this property is read when <see cref="HasValue"/> is false then it will return
/// <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
public object Value
{
get
{
if (_value != null)
{
object result;
if (_value.TryGetTarget(out result))
{
return result == NullValue ? null : result;
}
}
// There's the possibility of a race condition in that HasValue can return true,
// and then the value is GC'd before Value is read. We should be ok though as
// we return UnsetValue which should be a safe alternative.
return AvaloniaProperty.UnsetValue;
}
}
public object Value => _value;
/// <summary>
/// Gets a value indicating whether <see cref="Value"/> should be pushed to the target.
/// </summary>
public bool HasValue => _value != null;
public bool HasValue => _value != AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets the error that occurred on the source, if any.
@ -248,7 +226,7 @@ namespace Avalonia.Data
/// </summary>
public void ClearValue()
{
_value = null;
_value = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -256,7 +234,7 @@ namespace Avalonia.Data
/// </summary>
public void SetValue(object value)
{
_value = new WeakReference<object>(value ?? NullValue);
_value = value;
}
/// <inheritdoc/>

36
src/Avalonia.Controls/Button.cs

@ -207,7 +207,11 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
Command?.Execute(CommandParameter);
if (Command != null)
{
Command.Execute(CommandParameter);
e.Handled = true;
}
}
/// <inheritdoc/>
@ -215,13 +219,16 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
PseudoClasses.Add(":pressed");
e.Device.Capture(this);
e.Handled = true;
if (ClickMode == ClickMode.Press)
if (e.MouseButton == MouseButton.Left)
{
RaiseClickEvent();
PseudoClasses.Add(":pressed");
e.Device.Capture(this);
e.Handled = true;
if (ClickMode == ClickMode.Press)
{
RaiseClickEvent();
}
}
}
@ -230,13 +237,16 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
e.Device.Capture(null);
PseudoClasses.Remove(":pressed");
e.Handled = true;
if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover"))
if (e.MouseButton == MouseButton.Left)
{
RaiseClickEvent();
e.Device.Capture(null);
PseudoClasses.Remove(":pressed");
e.Handled = true;
if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this)))
{
RaiseClickEvent();
}
}
}

11
src/Avalonia.Controls/Control.cs

@ -118,6 +118,7 @@ namespace Avalonia.Controls
public Control()
{
_nameScope = this as INameScope;
_isAttachedToLogicalTree = this is IStyleRoot;
}
/// <summary>
@ -369,6 +370,12 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
this.OnAttachedToLogicalTreeCore(e);
}
/// <inheritdoc/>
void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
@ -418,7 +425,7 @@ namespace Avalonia.Controls
if (_isAttachedToLogicalTree)
{
var oldRoot = FindStyleRoot(old);
var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot;
if (oldRoot == null)
{
@ -436,7 +443,7 @@ namespace Avalonia.Controls
_parent = (IControl)parent;
if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true)
if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot)
{
var newRoot = FindStyleRoot(this);

2
src/Avalonia.Controls/Menu.cs

@ -47,7 +47,7 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick, handledEventsToo: true);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
}

11
src/Avalonia.Controls/MenuItem.cs

@ -102,6 +102,11 @@ namespace Avalonia.Controls
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(x => x.AccessKeyPressed);
}
public MenuItem()
{
}
/// <summary>
/// Occurs when a <see cref="MenuItem"/> without a submenu is clicked.
/// </summary>
@ -192,7 +197,11 @@ namespace Avalonia.Controls
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
Command?.Execute(CommandParameter);
if (Command != null)
{
Command.Execute(CommandParameter);
e.Handled = true;
}
}
/// <summary>

38
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
@ -88,6 +89,7 @@ namespace Avalonia.Controls.Presenters
static ContentPresenter()
{
ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged);
}
@ -313,27 +315,22 @@ namespace Avalonia.Controls.Presenters
if (content != null && newChild == null)
{
// We have content and it isn't a control, so first try to recycle the existing
// child control to display the new data by querying if the template that created
// the child can recycle items and that it also matches the new data.
if (oldChild != null &&
_dataTemplate != null &&
_dataTemplate.SupportsRecycling &&
_dataTemplate.Match(content))
var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
// We have content and it isn't a control, so if the new data template is the same
// as the old data template, try to recycle the existing child control to display
// the new data.
if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling)
{
newChild = oldChild;
}
else
{
// We couldn't recycle an existing control so find a data template for the data
// and use it to create a control.
_dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
_dataTemplate = dataTemplate;
newChild = _dataTemplate.Build(content);
// Try to give the new control its own name scope.
var controlResult = newChild as Control;
if (controlResult != null)
// Give the new control its own name scope.
if (newChild is Control controlResult)
{
NameScope.SetNameScope(controlResult, new NameScope());
}
@ -424,6 +421,19 @@ namespace Avalonia.Controls.Presenters
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
_createdChild = false;
if (((ILogical)this).IsAttachedToLogicalTree)
{
UpdateChild();
}
else if (Child != null)
{
VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
Child = null;
_dataTemplate = null;
}
InvalidateMeasure();
}

11
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -285,6 +285,17 @@ namespace Avalonia.Controls.Primitives
return this;
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (VisualChildren.Count > 0)
{
((ILogical)VisualChildren[0]).NotifyAttachedToLogicalTree(e);
}
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{

40
src/Avalonia.Controls/ToolTip.cs

@ -105,16 +105,21 @@ namespace Avalonia.Controls
{
if (control != null && control.IsVisible && control.GetVisualRoot() != null)
{
if (s_popup != null)
{
throw new AvaloniaInternalException("Previous ToolTip not disposed.");
}
var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control);
var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22);
s_popup = new PopupRoot();
if (s_popup == null)
{
s_popup = new PopupRoot();
s_popup.Content = new ToolTip();
}
else
{
((ISetLogicalParent)s_popup).SetParent(null);
}
((ISetLogicalParent)s_popup).SetParent(control);
s_popup.Content = new ToolTip { Content = GetTip(control) };
((ToolTip)s_popup.Content).Content = GetTip(control);
s_popup.Position = position;
s_popup.Show();
@ -146,16 +151,23 @@ namespace Avalonia.Controls
{
if (s_popup != null)
{
// Clear the ToolTip's Content in case it has control content: this will
// reset its visual parent allowing it to be used again.
((ToolTip)s_popup.Content).Content = null;
// Dispose of the popup.
s_popup.Dispose();
s_popup = null;
DisposeTooltip();
s_show.OnNext(null);
}
}
}
private static void DisposeTooltip()
{
if (s_popup != null)
{
// Clear the ToolTip's Content in case it has control content: this will
// reset its visual parent allowing it to be used again.
((ToolTip)s_popup.Content).Content = null;
s_show.OnNext(null);
// Dispose of the popup.
s_popup.Dispose();
s_popup = null;
}
}
}

22
src/Avalonia.Controls/TreeView.cs

@ -16,7 +16,7 @@ namespace Avalonia.Controls
/// <summary>
/// Displays a hierachical tree of data.
/// </summary>
public class TreeView : ItemsControl
public class TreeView : ItemsControl, ICustomKeyboardNavigation
{
/// <summary>
/// Defines the <see cref="AutoScrollToSelectedItem"/> property.
@ -90,6 +90,26 @@ namespace Avalonia.Controls
}
}
(bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction)
{
if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
if (!this.IsVisualAncestorOf(element))
{
IControl result = _selectedItem != null ?
ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) :
ItemContainerGenerator.ContainerFromIndex(0);
return (true, result);
}
else
{
return (true, null);
}
}
return (false, null);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{

3
src/Avalonia.Input/Avalonia.Input.csproj

@ -37,5 +37,8 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
</Project>

5
src/Avalonia.Input/FocusManager.cs

@ -176,9 +176,10 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
private void OnPreviewPointerPressed(object sender, RoutedEventArgs e)
{
if (sender == e.Source)
var ev = (PointerPressedEventArgs)e;
if (sender == e.Source && ev.MouseButton == MouseButton.Left)
{
var ev = (PointerPressedEventArgs)e;
var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement);
if (element == null || !CanFocus(element))

15
src/Avalonia.Input/ICustomKeyboardNavigation.cs

@ -0,0 +1,15 @@
// 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;
namespace Avalonia.Input
{
/// <summary>
/// Designates a control as handling its own keyboard navigation.
/// </summary>
public interface ICustomKeyboardNavigation
{
(bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
}
}

27
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Avalonia.Input.Navigation;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
@ -52,6 +54,31 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(element != null);
var customHandler = element.GetSelfAndVisualAncestors()
.OfType<ICustomKeyboardNavigation>()
.FirstOrDefault();
if (customHandler != null)
{
var (handled, next) = customHandler.GetNext(element, direction);
if (handled)
{
if (next != null)
{
return next;
}
else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true);
}
else
{
return null;
}
}
}
if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder(element, direction);

16
src/Avalonia.Input/Navigation/DirectionalNavigation.cs

@ -41,7 +41,7 @@ namespace Avalonia.Input.Navigation
{
case KeyboardNavigationMode.Continue:
return GetNextInContainer(element, container, direction) ??
GetFirstInNextContainer(element, direction);
GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
return GetNextInContainer(element, container, direction) ??
GetFocusableDescendant(container, direction);
@ -173,10 +173,12 @@ namespace Avalonia.Input.Navigation
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="element">The element being navigated away from.</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
@ -200,6 +202,16 @@ namespace Avalonia.Input.Navigation
if (sibling != null)
{
if (sibling is ICustomKeyboardNavigation custom)
{
var (handled, customNext) = custom.GetNext(element, direction);
if (handled)
{
return customNext;
}
}
if (sibling.CanFocus())
{
next = sibling;
@ -214,7 +226,7 @@ namespace Avalonia.Input.Navigation
if (next == null)
{
next = GetFirstInNextContainer(parent, direction);
next = GetFirstInNextContainer(element, parent, direction);
}
}
else

87
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -18,13 +18,17 @@ namespace Avalonia.Input.Navigation
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <param name="outsideElement">
/// If true will not descend into <paramref name="element"/> to find next control.
/// </param>
/// <returns>
/// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement GetNextInTabOrder(
IInputElement element,
NavigationDirection direction)
NavigationDirection direction,
bool outsideElement = false)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
@ -40,20 +44,20 @@ namespace Avalonia.Input.Navigation
switch (mode)
{
case KeyboardNavigationMode.Continue:
return GetNextInContainer(element, container, direction) ??
GetFirstInNextContainer(element, direction);
return GetNextInContainer(element, container, direction, outsideElement) ??
GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
return GetNextInContainer(element, container, direction) ??
return GetNextInContainer(element, container, direction, outsideElement) ??
GetFocusableDescendant(container, direction);
case KeyboardNavigationMode.Contained:
return GetNextInContainer(element, container, direction);
return GetNextInContainer(element, container, direction, outsideElement);
default:
return GetFirstInNextContainer(container, direction);
return GetFirstInNextContainer(element, container, direction);
}
}
else
{
return GetFocusableDescendants(element).FirstOrDefault();
return GetFocusableDescendants(element, direction).FirstOrDefault();
}
}
@ -66,16 +70,17 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
{
return direction == NavigationDirection.Next ?
GetFocusableDescendants(container).FirstOrDefault() :
GetFocusableDescendants(container).LastOrDefault();
GetFocusableDescendants(container, direction).FirstOrDefault() :
GetFocusableDescendants(container, direction).LastOrDefault();
}
/// <summary>
/// Gets the focusable descendants of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <returns>The element's focusable descendants.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element)
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element, NavigationDirection direction)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
@ -103,16 +108,25 @@ namespace Avalonia.Input.Navigation
foreach (var child in children)
{
if (child.CanFocus())
var customNext = GetCustomNext(child, direction);
if (customNext.handled)
{
yield return child;
yield return customNext.next;
}
if (child.CanFocusDescendants())
else
{
foreach (var descendant in GetFocusableDescendants(child))
if (child.CanFocus())
{
yield return descendant;
yield return child;
}
if (child.CanFocusDescendants())
{
foreach (var descendant in GetFocusableDescendants(child, direction))
{
yield return descendant;
}
}
}
}
@ -124,15 +138,19 @@ namespace Avalonia.Input.Navigation
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <param name="outsideElement">
/// If true will not descend into <paramref name="element"/> to find next control.
/// </param>
/// <returns>The next element, or null if the element is the last.</returns>
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
NavigationDirection direction,
bool outsideElement)
{
if (direction == NavigationDirection.Next)
if (direction == NavigationDirection.Next && !outsideElement)
{
var descendant = GetFocusableDescendants(element).FirstOrDefault();
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
if (descendant != null)
{
@ -167,7 +185,7 @@ namespace Avalonia.Input.Navigation
if (element != null && direction == NavigationDirection.Previous)
{
var descendant = GetFocusableDescendants(element).LastOrDefault();
var descendant = GetFocusableDescendants(element, direction).LastOrDefault();
if (descendant != null)
{
@ -184,10 +202,12 @@ namespace Avalonia.Input.Navigation
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="element">The element being navigated away from.</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
@ -210,6 +230,13 @@ namespace Avalonia.Input.Navigation
if (sibling != null)
{
var customNext = GetCustomNext(sibling, direction);
if (customNext.handled)
{
return customNext.next;
}
if (sibling.CanFocus())
{
next = sibling;
@ -217,24 +244,34 @@ namespace Avalonia.Input.Navigation
else
{
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(sibling).FirstOrDefault() :
GetFocusableDescendants(sibling).LastOrDefault();
GetFocusableDescendants(sibling, direction).FirstOrDefault() :
GetFocusableDescendants(sibling, direction).LastOrDefault();
}
}
if (next == null)
{
next = GetFirstInNextContainer(parent, direction);
next = GetFirstInNextContainer(element, parent, direction);
}
}
else
{
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(container).FirstOrDefault() :
GetFocusableDescendants(container).LastOrDefault();
GetFocusableDescendants(container, direction).FirstOrDefault() :
GetFocusableDescendants(container, direction).LastOrDefault();
}
return next;
}
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction)
{
if (element is ICustomKeyboardNavigation custom)
{
return custom.GetNext(element, direction);
}
return (false, null);
}
}
}

97
src/Avalonia.Layout/LayoutManager.cs

@ -14,8 +14,8 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager
{
private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private bool _queued;
private bool _running;
@ -30,8 +30,18 @@ namespace Avalonia.Layout
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();
_toMeasure.Add(control);
_toArrange.Add(control);
if (!control.IsAttachedToVisualTree)
{
#if DEBUG
throw new AvaloniaInternalException(
"LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree.");
#else
return;
#endif
}
_toMeasure.Enqueue(control);
_toArrange.Enqueue(control);
QueueLayoutPass();
}
@ -41,7 +51,17 @@ namespace Avalonia.Layout
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();
_toArrange.Add(control);
if (!control.IsAttachedToVisualTree)
{
#if DEBUG
throw new AvaloniaInternalException(
"LayoutManager.InvalidateArrange called on a control that is detached from the visual tree.");
#else
return;
#endif
}
_toArrange.Enqueue(control);
QueueLayoutPass();
}
@ -108,8 +128,12 @@ namespace Avalonia.Layout
{
while (_toMeasure.Count > 0)
{
var next = _toMeasure.First();
Measure(next);
var control = _toMeasure.Dequeue();
if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
{
Measure(control);
}
}
}
@ -117,53 +141,60 @@ namespace Avalonia.Layout
{
while (_toArrange.Count > 0 && _toMeasure.Count == 0)
{
var next = _toArrange.First();
Arrange(next);
var control = _toArrange.Dequeue();
if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
{
Arrange(control);
}
}
}
private void Measure(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Measure(root.MaxClientSize);
}
else if (parent != null)
// Controls closest to the visual root need to be arranged first. We don't try to store
// ordered invalidation lists, instead we traverse the tree upwards, measuring the
// controls closest to the root first. This has been shown by benchmarks to be the
// fastest and most memory-efficent algorithm.
if (control.VisualParent is ILayoutable parent)
{
Measure(parent);
}
if (!control.IsMeasureValid)
// If the control being measured has IsMeasureValid == true here then its measure was
// handed by an ancestor and can be ignored. The measure may have also caused the
// control to be removed.
if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
{
control.Measure(control.PreviousMeasure.Value);
if (control is ILayoutRoot root)
{
root.Measure(Size.Infinity);
}
else
{
control.Measure(control.PreviousMeasure.Value);
}
}
_toMeasure.Remove(control);
}
private void Arrange(ILayoutable control)
{
var root = control as ILayoutRoot;
var parent = control.VisualParent as ILayoutable;
if (root != null)
{
root.Arrange(new Rect(root.ClientSize));
}
else if (parent != null)
if (control.VisualParent is ILayoutable parent)
{
Arrange(parent);
}
if (control.PreviousArrange.HasValue)
if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
{
control.Arrange(control.PreviousArrange.Value);
if (control is ILayoutRoot root)
{
root.Arrange(new Rect(root.ClientSize));
}
else
{
control.Arrange(control.PreviousArrange.Value);
}
}
_toArrange.Remove(control);
}
private void QueueLayoutPass()

16
src/Avalonia.Layout/Layoutable.cs

@ -378,8 +378,12 @@ namespace Avalonia.Layout
IsMeasureValid = false;
IsArrangeValid = false;
LayoutManager.Instance?.InvalidateMeasure(this);
InvalidateVisual();
if (((ILayoutable)this).IsAttachedToVisualTree)
{
LayoutManager.Instance?.InvalidateMeasure(this);
InvalidateVisual();
}
}
}
@ -393,8 +397,12 @@ namespace Avalonia.Layout
Logger.Verbose(LogArea.Layout, this, "Invalidated arrange");
IsArrangeValid = false;
LayoutManager.Instance?.InvalidateArrange(this);
InvalidateVisual();
if (((ILayoutable)this).IsAttachedToVisualTree)
{
LayoutManager.Instance?.InvalidateArrange(this);
InvalidateVisual();
}
}
}

10
src/Avalonia.Styling/LogicalTree/ILogical.cs

@ -36,6 +36,16 @@ namespace Avalonia.LogicalTree
/// </summary>
IAvaloniaReadOnlyList<ILogical> LogicalChildren { get; }
/// <summary>
/// Notifies the control that it is being attached to a rooted logical tree.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method will be called automatically by the framework, you should not need to call
/// this method yourself.
/// </remarks>
void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e);
/// <summary>
/// Notifies the control that it is being detached from a rooted logical tree.
/// </summary>

4
src/Avalonia.Styling/Styling/Style.cs

@ -61,12 +61,12 @@ namespace Avalonia.Styling
}
/// <summary>
/// Gets or sets style's selector.
/// Gets or sets the style's selector.
/// </summary>
public Selector Selector { get; set; }
/// <summary>
/// Gets or sets style's setters.
/// Gets or sets the style's setters.
/// </summary>
[Content]
public IEnumerable<ISetter> Setters { get; set; } = new List<ISetter>();

2
src/Gtk/Avalonia.Gtk3/Interop/GObject.cs

@ -41,7 +41,7 @@ namespace Avalonia.Gtk3.Interop
class GtkWindow : GtkWidget
{
public static GtkWindow Null { get; } = new GtkWindow();
}
class GtkImContext : GObject

2
src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs

@ -11,6 +11,8 @@ namespace Avalonia.Gtk3.Interop
public Utf8Buffer(string s) : base(IntPtr.Zero, true)
{
if (s == null)
return;
_data = Encoding.UTF8.GetBytes(s);
_gchandle = GCHandle.Alloc(_data, GCHandleType.Pinned);
handle = _gchandle.AddrOfPinnedObject();

3
src/Gtk/Avalonia.Gtk3/SystemDialogs.cs

@ -18,7 +18,8 @@ namespace Avalonia.Gtk3
bool multiselect, string initialFileName)
{
GtkFileChooser dlg;
using (var name = title != null ? new Utf8Buffer(title) : null)
parent = parent ?? GtkWindow.Null;
using (var name = new Utf8Buffer(title))
dlg = Native.GtkFileChooserDialogNew(name, parent, action, IntPtr.Zero);
if (multiselect)
Native.GtkFileChooserSetSelectMultiple(dlg, true);

2
src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs

@ -102,7 +102,7 @@ namespace Avalonia.Markup.Xaml.Data
private object ConvertValue(IList<object> values, Type targetType)
{
var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentUICulture);
var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentCulture);
if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null)
{

4
src/Markup/Avalonia.Markup/Data/BindingExpression.cs

@ -122,7 +122,7 @@ namespace Avalonia.Markup.Data
value,
type,
ConverterParameter,
CultureInfo.CurrentUICulture);
CultureInfo.CurrentCulture);
if (converted == AvaloniaProperty.UnsetValue)
{
@ -186,7 +186,7 @@ namespace Avalonia.Markup.Data
value,
_targetType,
ConverterParameter,
CultureInfo.CurrentUICulture);
CultureInfo.CurrentCulture);
notification = converted as BindingNotification;

1
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

2
tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs

@ -7,4 +7,4 @@ using Xunit;
[assembly: AssemblyTitle("Avalonia.UnitTests")]
// Don't run tests in parallel.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: CollectionBehavior(DisableTestParallelization = true)]

3
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -49,6 +49,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Layout\Measure.cs" />
<Compile Include="Styling\ApplyStyling.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@ -100,7 +101,7 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.9.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
</Project>

65
tests/Avalonia.Benchmarks/Layout/Measure.cs

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Layout
{
[MemoryDiagnoser]
public class Measure : IDisposable
{
private IDisposable _app;
private TestRoot root;
private List<Control> controls = new List<Control>();
public Measure()
{
_app = UnitTestApplication.Start(TestServices.RealLayoutManager);
var panel = new StackPanel();
root = new TestRoot { Child = panel };
controls.Add(panel);
CreateChildren(panel, 3, 5);
LayoutManager.Instance.ExecuteInitialLayoutPass(root);
}
public void Dispose()
{
_app.Dispose();
}
[Benchmark]
public void Remeasure_Half()
{
var random = new Random(1);
foreach (var control in controls)
{
if (random.Next(2) == 0)
{
control.InvalidateMeasure();
}
}
LayoutManager.Instance.ExecuteLayoutPass();
}
private void CreateChildren(IPanel parent, int childCount, int iterations)
{
for (var i = 0; i < childCount; ++i)
{
var control = new StackPanel();
parent.Children.Add(control);
if (iterations > 0)
{
CreateChildren(control, childCount, iterations - 1);
}
controls.Add(control);
}
}
}
}

1
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@ -11,6 +11,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class ApplyStyling : IDisposable
{
private IDisposable _app;

4
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -1,12 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<ItemGroup>
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

291
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs

@ -0,0 +1,291 @@
// 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.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
/// <summary>
/// Tests for ContentControls that are hosted in a control template.
/// </summary>
public class ContentPresenterTests_InTemplate
{
[Fact]
public void Should_Register_With_Host_When_TemplatedParent_Set()
{
var host = new Mock<IContentPresenterHost>();
var target = new ContentPresenter();
target.SetValue(Control.TemplatedParentProperty, host.Object);
host.Verify(x => x.RegisterContentPresenter(target));
}
[Fact]
public void Setting_Content_To_Control_Should_Set_Child()
{
var (target, _) = CreateTarget();
var child = new Border();
target.Content = child;
Assert.Equal(child, target.Child);
}
[Fact]
public void Setting_Content_To_Control_Should_Update_Logical_Tree()
{
var (target, parent) = CreateTarget();
var child = new Border();
target.Content = child;
Assert.Equal(parent, child.GetLogicalParent());
Assert.Equal(new[] { child }, parent.GetLogicalChildren());
}
[Fact]
public void Setting_Content_To_Control_Should_Update_Visual_Tree()
{
var (target, _) = CreateTarget();
var child = new Border();
target.Content = child;
Assert.Equal(target, child.GetVisualParent());
Assert.Equal(new[] { child }, target.GetVisualChildren());
}
[Fact]
public void Setting_Content_To_String_Should_Create_TextBlock()
{
var (target, _) = CreateTarget();
target.Content = "Foo";
Assert.IsType<TextBlock>(target.Child);
Assert.Equal("Foo", ((TextBlock)target.Child).Text);
}
[Fact]
public void Setting_Content_To_String_Should_Update_Logical_Tree()
{
var (target, parent) = CreateTarget();
target.Content = "Foo";
var child = target.Child;
Assert.Equal(parent, child.GetLogicalParent());
Assert.Equal(new[] { child }, parent.GetLogicalChildren());
}
[Fact]
public void Setting_Content_To_String_Should_Update_Visual_Tree()
{
var (target, _) = CreateTarget();
target.Content = "Foo";
var child = target.Child;
Assert.Equal(target, child.GetVisualParent());
Assert.Equal(new[] { child }, target.GetVisualChildren());
}
[Fact]
public void Clearing_Control_Content_Should_Update_Logical_Tree()
{
var (target, _) = CreateTarget();
var child = new Border();
target.Content = child;
target.Content = null;
Assert.Equal(null, child.GetLogicalParent());
Assert.Empty(target.GetLogicalChildren());
}
[Fact]
public void Clearing_Control_Content_Should_Update_Visual_Tree()
{
var (target, _) = CreateTarget();
var child = new Border();
target.Content = child;
target.Content = null;
Assert.Equal(null, child.GetVisualParent());
Assert.Empty(target.GetVisualChildren());
}
[Fact]
public void Control_Content_Should_Not_Be_NameScope()
{
var (target, _) = CreateTarget();
target.Content = new TextBlock();
Assert.IsType<TextBlock>(target.Child);
Assert.Null(NameScope.GetNameScope((Control)target.Child));
}
[Fact]
public void DataTemplate_Created_Control_Should_Be_NameScope()
{
var (target, _) = CreateTarget();
target.Content = "Foo";
Assert.IsType<TextBlock>(target.Child);
Assert.NotNull(NameScope.GetNameScope((Control)target.Child));
}
[Fact]
public void Assigning_Control_To_Content_Should_Not_Set_DataContext()
{
var (target, _) = CreateTarget();
target.Content = new Border();
Assert.False(target.IsSet(Control.DataContextProperty));
}
[Fact]
public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild()
{
var (target, _) = CreateTarget();
target.Content = "foo";
Assert.Equal("foo", target.DataContext);
}
[Fact]
public void Should_Use_ContentTemplate_If_Specified()
{
var (target, _) = CreateTarget();
target.ContentTemplate = new FuncDataTemplate<string>(_ => new Canvas());
target.Content = "Foo";
Assert.IsType<Canvas>(target.Child);
}
[Fact]
public void Should_Update_If_ContentTemplate_Changed()
{
var (target, _) = CreateTarget();
target.Content = "Foo";
Assert.IsType<TextBlock>(target.Child);
target.ContentTemplate = new FuncDataTemplate<string>(_ => new Canvas());
Assert.IsType<Canvas>(target.Child);
target.ContentTemplate = null;
Assert.IsType<TextBlock>(target.Child);
}
[Fact]
public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext()
{
var (target, _) = CreateTarget();
target.Content = "foo";
Assert.True(target.IsSet(Control.DataContextProperty));
target.Content = new Border();
Assert.False(target.IsSet(Control.DataContextProperty));
}
[Fact]
public void Recycles_DataTemplate()
{
var (target, _) = CreateTarget();
target.DataTemplates.Add(new FuncDataTemplate<string>(_ => new Border(), true));
target.Content = "foo";
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
Assert.Same(control, target.Child);
}
[Fact]
public void Detects_DataTemplate_Doesnt_Match_And_Doesnt_Recycle()
{
var (target, _) = CreateTarget();
target.DataTemplates.Add(new FuncDataTemplate<string>(x => x == "foo", _ => new Border(), true));
target.Content = "foo";
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
Assert.IsType<TextBlock>(target.Child);
}
[Fact]
public void Detects_DataTemplate_Doesnt_Support_Recycling()
{
var (target, _) = CreateTarget();
target.DataTemplates.Add(new FuncDataTemplate<string>(_ => new Border(), false));
target.Content = "foo";
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
Assert.NotSame(control, target.Child);
}
[Fact]
public void Reevaluates_DataTemplates_When_Recycling()
{
var (target, _) = CreateTarget();
target.DataTemplates.Add(new FuncDataTemplate<string>(x => x == "bar", _ => new Canvas(), true));
target.DataTemplates.Add(new FuncDataTemplate<string>(_ => new Border(), true));
target.Content = "foo";
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
Assert.IsType<Canvas>(target.Child);
}
(ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
{
var templatedParent = new ContentControl
{
Template = new FuncControlTemplate<ContentControl>(x =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
}),
};
var root = new TestRoot { Child = templatedParent };
templatedParent.ApplyTemplate();
return ((ContentPresenter)templatedParent.Presenter, templatedParent);
}
private class TestContentControl : ContentControl
{
public IControl Child { get; set; }
}
}
}

190
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs → tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

@ -15,91 +15,13 @@ using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ContentPresenterTests
/// <summary>
/// Tests for ContentControls that aren't hosted in a control template.
/// </summary>
public class ContentPresenterTests_Standalone
{
[Fact]
public void Should_Register_With_Host_When_TemplatedParent_Set()
{
var host = new Mock<IContentPresenterHost>();
var target = new ContentPresenter();
target.SetValue(Control.TemplatedParentProperty, host.Object);
host.Verify(x => x.RegisterContentPresenter(target));
}
[Fact]
public void Setting_Content_To_Control_Should_Set_Child()
{
var target = new ContentPresenter();
var child = new Border();
target.Content = child;
Assert.Null(target.Child);
target.UpdateChild();
Assert.Equal(child, target.Child);
}
[Fact]
public void Setting_Content_To_String_Should_Create_TextBlock()
{
var target = new ContentPresenter();
target.Content = "Foo";
Assert.Null(target.Child);
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
Assert.Equal("Foo", ((TextBlock)target.Child).Text);
}
[Fact]
public void Control_Content_Should_Not_Be_NameScope()
{
var target = new ContentPresenter();
target.Content = new TextBlock();
Assert.Null(target.Child);
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
Assert.Null(NameScope.GetNameScope((Control)target.Child));
}
[Fact]
public void DataTemplate_Created_Control_Should_Be_NameScope()
{
var target = new ContentPresenter();
target.Content = "Foo";
Assert.Null(target.Child);
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
Assert.NotNull(NameScope.GetNameScope((Control)target.Child));
}
[Fact]
public void Should_Set_Childs_Parent_To_TemplatedParent()
{
var content = new Border();
var target = new TestContentControl
{
Template = new FuncControlTemplate<TestContentControl>(parent =>
new ContentPresenter { Content = parent.Child }),
Child = content,
};
target.ApplyTemplate();
var presenter = ((ContentPresenter)target.GetVisualChildren().Single());
presenter.UpdateChild();
Assert.Same(target, content.Parent);
}
[Fact]
public void Should_Set_Childs_Parent_To_Itself_Outside_Template()
public void Should_Set_Childs_Parent_To_Itself_Standalone()
{
var content = new Border();
var target = new ContentPresenter { Content = content };
@ -110,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Should_Add_Child_To_Own_LogicalChildren_Outside_Template()
public void Should_Add_Child_To_Own_LogicalChildren_Standalone()
{
var content = new Border();
var target = new ContentPresenter { Content = content };
@ -124,94 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates()
{
var target = new ContentPresenter
{
Content = "Foo",
};
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
var root = new TestRoot
{
DataTemplates = new DataTemplates
{
new FuncDataTemplate<string>(x => new Decorator()),
},
};
root.Child = target;
target.ApplyTemplate();
Assert.IsType<Decorator>(target.Child);
}
[Fact]
public void Assigning_Control_To_Content_Should_Not_Set_DataContext()
{
var target = new ContentPresenter
{
Content = new Border(),
};
Assert.False(target.IsSet(Control.DataContextProperty));
}
[Fact]
public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild()
{
var target = new ContentPresenter
{
Content = "foo",
};
target.UpdateChild();
Assert.Equal("foo", target.DataContext);
}
[Fact]
public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext()
{
var target = new ContentPresenter();
target.Content = "foo";
target.UpdateChild();
Assert.True(target.IsSet(Control.DataContextProperty));
target.Content = new Border();
target.UpdateChild();
Assert.False(target.IsSet(Control.DataContextProperty));
}
[Fact]
public void Tries_To_Recycle_DataTemplate()
{
var target = new ContentPresenter
{
DataTemplates = new DataTemplates
{
new FuncDataTemplate<string>(_ => new Border(), true),
},
Content = "foo",
};
target.UpdateChild();
var control = target.Child;
Assert.IsType<Border>(control);
target.Content = "bar";
target.UpdateChild();
Assert.Same(control, target.Child);
}
[Fact]
public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_OutsideTemplate()
public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_Standalone()
{
var target = new ContentPresenter
{
@ -250,7 +85,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_OutsideTemplate()
public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_Standalone()
{
var contentControl = new ContentControl
{
@ -292,13 +127,14 @@ namespace Avalonia.Controls.UnitTests.Presenters
var tbbar = target.Child as ContentControl;
Assert.NotNull(tbbar);
Assert.True(tbbar != tbfoo);
Assert.False((tbfoo as IControl).IsAttachedToLogicalTree);
Assert.True(foodetached);
}
[Fact]
public void Should_Raise_DetachedFromLogicalTree_On_Detached_OutsideTemplate()
public void Should_Raise_DetachedFromLogicalTree_On_Detached_Standalone()
{
var target = new ContentPresenter
{
@ -332,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_OutsideTemplate()
public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_Standalone()
{
var target = new ContentPresenter
{
@ -363,9 +199,5 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.NotEqual(foo, logicalChildren.First());
}
private class TestContentControl : ContentControl
{
public IControl Child { get; set; }
}
}
}

102
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs

@ -0,0 +1,102 @@
// 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 Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
/// <summary>
/// Tests for ContentControls that are not attached to a logical tree.
/// </summary>
public class ContentPresenterTests_Unrooted
{
[Fact]
public void Setting_Content_To_Control_Should_Not_Set_Child_Unless_UpdateChild_Called()
{
var target = new ContentPresenter();
var child = new Border();
target.Content = child;
Assert.Null(target.Child);
target.ApplyTemplate();
Assert.Null(target.Child);
target.UpdateChild();
Assert.Equal(child, target.Child);
}
[Fact]
public void Setting_Content_To_String_Should_Not_Create_TextBlock_Unless_UpdateChild_Called()
{
var target = new ContentPresenter();
target.Content = "Foo";
Assert.Null(target.Child);
target.ApplyTemplate();
Assert.Null(target.Child);
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
Assert.Equal("Foo", ((TextBlock)target.Child).Text);
}
[Fact]
public void Clearing_Control_Content_Should_Remove_Child_Immediately()
{
var target = new ContentPresenter();
var child = new Border();
target.Content = child;
target.UpdateChild();
Assert.Equal(child, target.Child);
target.Content = null;
Assert.Null(target.Child);
}
[Fact]
public void Clearing_String_Content_Should_Remove_Child_Immediately()
{
var target = new ContentPresenter();
target.Content = "Foo";
target.UpdateChild();
Assert.IsType<TextBlock>(target.Child);
target.Content = null;
Assert.Null(target.Child);
}
[Fact]
public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates()
{
var root = new TestRoot();
var target = new ContentPresenter();
target.Content = "Foo";
Assert.Null(target.Child);
root.Child = target;
target.ApplyTemplate();
Assert.IsType<TextBlock>(target.Child);
root.Child = null;
root = new TestRoot
{
DataTemplates = new DataTemplates
{
new FuncDataTemplate<string>(x => new Decorator()),
},
};
root.Child = target;
target.ApplyTemplate();
Assert.IsType<Decorator>(target.Child);
}
}
}

109
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -0,0 +1,109 @@
// 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 Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
{
public class PopupRootTests
{
[Fact]
public void PopupRoot_IsAttachedToLogicalTree_Is_True()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = CreateTarget();
Assert.True(((ILogical)target).IsAttachedToLogicalTree);
}
}
[Fact]
public void Templated_Child_IsAttachedToLogicalTree_Is_True()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = CreateTarget();
Assert.True(target.Presenter.IsAttachedToLogicalTree);
}
}
[Fact]
public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Decorator();
var target = CreateTarget();
var window = new Window();
var detachedCount = 0;
var attachedCount = 0;
target.Content = child;
target.DetachedFromLogicalTree += (s, e) => ++detachedCount;
child.DetachedFromLogicalTree += (s, e) => ++detachedCount;
target.AttachedToLogicalTree += (s, e) => ++attachedCount;
child.AttachedToLogicalTree += (s, e) => ++attachedCount;
((ISetLogicalParent)target).SetParent(window);
Assert.Equal(2, detachedCount);
Assert.Equal(2, attachedCount);
}
}
[Fact]
public void Detaching_PopupRoot_From_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Decorator();
var target = CreateTarget();
var window = new Window();
var detachedCount = 0;
var attachedCount = 0;
target.Content = child;
((ISetLogicalParent)target).SetParent(window);
target.DetachedFromLogicalTree += (s, e) => ++detachedCount;
child.DetachedFromLogicalTree += (s, e) => ++detachedCount;
target.AttachedToLogicalTree += (s, e) => ++attachedCount;
child.AttachedToLogicalTree += (s, e) => ++attachedCount;
((ISetLogicalParent)target).SetParent(null);
// Despite being detached from the parent logical tree, we're still attached to a
// logical tree as PopupRoot itself is a logical tree root.
Assert.True(((ILogical)target).IsAttachedToLogicalTree);
Assert.True(((ILogical)child).IsAttachedToLogicalTree);
Assert.Equal(2, detachedCount);
Assert.Equal(2, attachedCount);
}
}
private PopupRoot CreateTarget()
{
var result = new PopupRoot
{
Template = new FuncControlTemplate<PopupRoot>(_ =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
}),
};
result.ApplyTemplate();
return result;
}
}
}

28
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -527,6 +527,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
[Fact]
public void Moving_To_New_LogicalTree_Should_Detach_Attach_Template_Child()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
TestTemplatedControl target;
var root = new TestRoot
{
Child = target = new TestTemplatedControl
{
Template = new FuncControlTemplate(_ => new Decorator()),
}
};
Assert.NotNull(target.Template);
target.ApplyTemplate();
var templateChild = (ILogical)target.GetVisualChildren().Single();
Assert.True(templateChild.IsAttachedToLogicalTree);
root.Child = null;
Assert.False(templateChild.IsAttachedToLogicalTree);
var newRoot = new TestRoot { Child = target };
Assert.True(templateChild.IsAttachedToLogicalTree);
}
}
private static IControl ScrollingContentControlTemplate(ContentControl control)
{
return new Border

19
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -2,24 +2,33 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Subjects;
using Moq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class TopLevelTests
{
[Fact]
public void IsAttachedToLogicalTree_Is_True()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var impl = new Mock<ITopLevelImpl>();
var target = new TestTopLevel(impl.Object);
Assert.True(((ILogical)target).IsAttachedToLogicalTree);
}
}
[Fact]
public void ClientSize_Should_Be_Set_On_Construction()
{

44
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -315,6 +315,50 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1));
}
[Fact]
public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var focus = FocusManager.Instance;
var navigation = AvaloniaLocator.Current.GetService<IKeyboardNavigationHandler>();
var data = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = data,
DataTemplates = CreateNodeDataTemplate(),
};
var button = new Button();
var root = new TestRoot
{
Child = new StackPanel
{
Children = { target, button },
}
};
ApplyTemplates(target);
var item = data[0].Children[0];
var node = target.ItemContainerGenerator.Index.ContainerFromItem(item);
Assert.NotNull(node);
target.SelectedItem = item;
node.Focus();
Assert.Same(node, focus.Current);
navigation.Move(focus.Current, NavigationDirection.Next);
Assert.Same(button, focus.Current);
navigation.Move(focus.Current, NavigationDirection.Next);
Assert.Same(node, focus.Current);
}
}
private void ApplyTemplates(TreeView tree)
{
tree.ApplyTemplate();

1
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

214
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs

@ -0,0 +1,214 @@
// 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 Avalonia.Controls;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class KeyboardNavigationTests_Custom
{
[Fact]
public void Tab_Should_Custom_Navigate_Within_Children()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
(current = new Button { Content = "Button 1" }),
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(next, result);
}
[Fact]
public void Right_Should_Custom_Navigate_Within_Children()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
(current = new Button { Content = "Button 1" }),
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right);
Assert.Same(next, result);
}
[Fact]
public void Tab_Should_Custom_Navigate_From_Outside()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var root = new StackPanel
{
Children =
{
(current = new Button { Content = "Outside" }),
target,
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(next, result);
}
[Fact]
public void Tab_Should_Custom_Navigate_From_Outside_When_Wrapping()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var root = new StackPanel
{
Children =
{
target,
(current = new Button { Content = "Outside" }),
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(next, result);
}
[Fact]
public void ShiftTab_Should_Custom_Navigate_From_Outside()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var root = new StackPanel
{
Children =
{
(current = new Button { Content = "Outside" }),
target,
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
Assert.Same(next, result);
}
[Fact]
public void Right_Should_Custom_Navigate_From_Outside()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var root = new StackPanel
{
Children =
{
(current = new Button { Content = "Outside" }),
target,
},
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right);
Assert.Same(next, result);
}
[Fact]
public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
(current = new Button { Content = "Button 2" }),
new Button { Content = "Button 3" },
},
};
var root = new StackPanel
{
Children =
{
target,
(next = new Button { Content = "Outside" }),
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(next, result);
}
private class CustomNavigatingStackPanel : StackPanel, ICustomKeyboardNavigation
{
public bool CustomNavigates { get; set; } = true;
public IInputElement NextControl { get; set; }
public (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction)
{
return (CustomNavigates, NextControl);
}
}
}
}

1
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\XUnit.props" />

1
tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

272
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@ -2,25 +2,276 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Avalonia.UnitTests;
using System;
using Xunit;
using System.Collections.Generic;
namespace Avalonia.Layout.UnitTests
{
public class LayoutManagerTests
{
[Fact]
public void Invalidating_Child_Should_Remeasure_Parent()
public void Measures_And_Arranges_InvalidateMeasured_Control()
{
var layoutManager = new LayoutManager();
var target = new LayoutManager();
using (AvaloniaLocator.EnterScope())
using (Start(target))
{
AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
var control = new LayoutTestControl();
var root = new LayoutTestRoot { Child = control };
target.ExecuteInitialLayoutPass(root);
control.Measured = control.Arranged = false;
control.InvalidateMeasure();
target.ExecuteLayoutPass();
Assert.True(control.Measured);
Assert.True(control.Arranged);
}
}
[Fact]
public void Arranges_InvalidateArranged_Control()
{
var target = new LayoutManager();
using (Start(target))
{
var control = new LayoutTestControl();
var root = new LayoutTestRoot { Child = control };
target.ExecuteInitialLayoutPass(root);
control.Measured = control.Arranged = false;
control.InvalidateArrange();
target.ExecuteLayoutPass();
Assert.False(control.Measured);
Assert.True(control.Arranged);
}
}
[Fact]
public void Measures_Parent_Of_Newly_Added_Control()
{
var target = new LayoutManager();
using (Start(target))
{
var control = new LayoutTestControl();
var root = new LayoutTestRoot();
target.ExecuteInitialLayoutPass(root);
root.Child = control;
root.Measured = root.Arranged = false;
target.ExecuteLayoutPass();
Assert.True(root.Measured);
Assert.True(root.Arranged);
Assert.True(control.Measured);
Assert.True(control.Arranged);
}
}
[Fact]
public void Measures_In_Correct_Order()
{
var target = new LayoutManager();
using (Start(target))
{
LayoutTestControl control1;
LayoutTestControl control2;
var root = new LayoutTestRoot
{
Child = control1 = new LayoutTestControl
{
Child = control2 = new LayoutTestControl(),
}
};
var order = new List<ILayoutable>();
Size MeasureOverride(ILayoutable control, Size size)
{
order.Add(control);
return new Size(10, 10);
}
root.DoMeasureOverride = MeasureOverride;
control1.DoMeasureOverride = MeasureOverride;
control2.DoMeasureOverride = MeasureOverride;
target.ExecuteInitialLayoutPass(root);
control2.InvalidateMeasure();
control1.InvalidateMeasure();
root.InvalidateMeasure();
order.Clear();
target.ExecuteLayoutPass();
Assert.Equal(new ILayoutable[] { root, control1, control2 }, order);
}
}
[Fact]
public void Measures_Root_And_Grandparent_In_Correct_Order()
{
var target = new LayoutManager();
using (Start(target))
{
LayoutTestControl control1;
LayoutTestControl control2;
var root = new LayoutTestRoot
{
Child = control1 = new LayoutTestControl
{
Child = control2 = new LayoutTestControl(),
}
};
var order = new List<ILayoutable>();
Size MeasureOverride(ILayoutable control, Size size)
{
order.Add(control);
return new Size(10, 10);
}
root.DoMeasureOverride = MeasureOverride;
control1.DoMeasureOverride = MeasureOverride;
control2.DoMeasureOverride = MeasureOverride;
target.ExecuteInitialLayoutPass(root);
control2.InvalidateMeasure();
root.InvalidateMeasure();
order.Clear();
target.ExecuteLayoutPass();
Assert.Equal(new ILayoutable[] { root, control2 }, order);
}
}
[Fact]
public void Doesnt_Measure_Non_Invalidated_Root()
{
var target = new LayoutManager();
using (Start(target))
{
var control = new LayoutTestControl();
var root = new LayoutTestRoot { Child = control };
target.ExecuteInitialLayoutPass(root);
root.Measured = root.Arranged = false;
control.Measured = control.Arranged = false;
control.InvalidateMeasure();
target.ExecuteLayoutPass();
Assert.False(root.Measured);
Assert.False(root.Arranged);
Assert.True(control.Measured);
Assert.True(control.Arranged);
}
}
[Fact]
public void Doesnt_Measure_Removed_Control()
{
var target = new LayoutManager();
using (Start(target))
{
var control = new LayoutTestControl();
var root = new LayoutTestRoot { Child = control };
target.ExecuteInitialLayoutPass(root);
control.Measured = control.Arranged = false;
control.InvalidateMeasure();
root.Child = null;
target.ExecuteLayoutPass();
Assert.False(control.Measured);
Assert.False(control.Arranged);
}
}
[Fact]
public void Measures_Root_With_Infinity()
{
var target = new LayoutManager();
using (Start(target))
{
var root = new LayoutTestRoot();
var availableSize = default(Size);
// Should not measure with this size.
root.MaxClientSize = new Size(123, 456);
root.DoMeasureOverride = (_, s) =>
{
availableSize = s;
return new Size(100, 100);
};
target.ExecuteInitialLayoutPass(root);
Assert.Equal(Size.Infinity, availableSize);
}
}
[Fact]
public void Arranges_Root_With_DesiredSize()
{
var target = new LayoutManager();
using (Start(target))
{
var root = new LayoutTestRoot
{
Width = 100,
Height = 100,
};
var arrangeSize = default(Size);
root.DoArrangeOverride = (_, s) =>
{
arrangeSize = s;
return s;
};
target.ExecuteInitialLayoutPass(root);
Assert.Equal(new Size(100, 100), arrangeSize);
root.Width = 120;
target.ExecuteLayoutPass();
Assert.Equal(new Size(120, 100), arrangeSize);
}
}
[Fact]
public void Invalidating_Child_Remeasures_Parent()
{
var target = new LayoutManager();
using (Start(target))
{
AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(target);
Border border;
StackPanel panel;
var root = new TestLayoutRoot
var root = new LayoutTestRoot
{
Child = panel = new StackPanel
{
@ -31,15 +282,22 @@ namespace Avalonia.Layout.UnitTests
}
};
layoutManager.ExecuteInitialLayoutPass(root);
target.ExecuteInitialLayoutPass(root);
Assert.Equal(new Size(0, 0), root.DesiredSize);
border.Width = 100;
border.Height = 100;
layoutManager.ExecuteLayoutPass();
target.ExecuteLayoutPass();
Assert.Equal(new Size(100, 100), panel.DesiredSize);
}
}
private IDisposable Start(LayoutManager layoutManager)
{
var result = AvaloniaLocator.EnterScope();
AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
return result;
}
}
}

29
tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs

@ -0,0 +1,29 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Layout.UnitTests
{
internal class LayoutTestControl : Decorator
{
public bool Measured { get; set; }
public bool Arranged { get; set; }
public Func<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
public Func<ILayoutable, Size, Size> DoArrangeOverride { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
Measured = true;
return DoMeasureOverride != null ?
DoMeasureOverride(this, availableSize) :
base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
Arranged = true;
return DoArrangeOverride != null ?
DoArrangeOverride(this, finalSize) :
base.ArrangeOverride(finalSize);
}
}
}

43
tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs

@ -0,0 +1,43 @@
// 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 Avalonia.UnitTests;
namespace Avalonia.Layout.UnitTests
{
internal class LayoutTestRoot : TestRoot, ILayoutable
{
public bool Measured { get; set; }
public bool Arranged { get; set; }
public Func<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
public Func<ILayoutable, Size, Size> DoArrangeOverride { get; set; }
void ILayoutable.Measure(Size availableSize)
{
Measured = true;
Measure(availableSize);
}
void ILayoutable.Arrange(Rect rect)
{
Arranged = true;
Arrange(rect);
}
protected override Size MeasureOverride(Size availableSize)
{
return DoMeasureOverride != null ?
DoMeasureOverride(this, availableSize) :
base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
Arranged = true;
return DoArrangeOverride != null ?
DoArrangeOverride(this, finalSize) :
base.ArrangeOverride(finalSize);
}
}
}

90
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@ -0,0 +1,90 @@
using System;
using Avalonia.Controls;
using Moq;
using Xunit;
namespace Avalonia.Layout.UnitTests
{
public class LayoutableTests
{
[Fact]
public void Only_Calls_LayoutManager_InvalidateMeasure_Once()
{
var target = new Mock<ILayoutManager>();
using (Start(target.Object))
{
var control = new Decorator();
var root = new LayoutTestRoot { Child = control };
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
target.ResetCalls();
control.InvalidateMeasure();
control.InvalidateMeasure();
target.Verify(x => x.InvalidateMeasure(control), Times.Once());
}
}
[Fact]
public void Only_Calls_LayoutManager_InvalidateArrange_Once()
{
var target = new Mock<ILayoutManager>();
using (Start(target.Object))
{
var control = new Decorator();
var root = new LayoutTestRoot { Child = control };
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
target.ResetCalls();
control.InvalidateArrange();
control.InvalidateArrange();
target.Verify(x => x.InvalidateArrange(control), Times.Once());
}
}
[Fact]
public void Attaching_Control_To_Tree_Invalidates_Parent_Measure()
{
var target = new Mock<ILayoutManager>();
using (Start(target.Object))
{
var control = new Decorator();
var root = new LayoutTestRoot { Child = control };
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
Assert.True(control.IsMeasureValid);
root.Child = null;
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
Assert.False(control.IsMeasureValid);
Assert.True(root.IsMeasureValid);
target.ResetCalls();
root.Child = control;
Assert.False(root.IsMeasureValid);
Assert.False(control.IsMeasureValid);
target.Verify(x => x.InvalidateMeasure(root), Times.Once());
}
}
private IDisposable Start(ILayoutManager layoutManager)
{
var result = AvaloniaLocator.EnterScope();
AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
return result;
}
}
}

24
tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs

@ -1,24 +0,0 @@
// 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 Avalonia.Controls;
namespace Avalonia.Layout.UnitTests
{
internal class TestLayoutRoot : Decorator, ILayoutRoot
{
public TestLayoutRoot()
{
ClientSize = new Size(500, 500);
}
public Size ClientSize
{
get;
set;
}
public Size MaxClientSize => Size.Infinity;
public double LayoutScaling => 1;
}
}

1
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

3
tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.UnitTests;
using Xunit;
@ -13,7 +14,7 @@ namespace Avalonia.Markup.UnitTests
public class ControlLocatorTests
{
[Fact]
public async void Track_By_Name_Should_Find_Control_Added_Earlier()
public async Task Track_By_Name_Should_Find_Control_Added_Earlier()
{
TextBlock target;
TextBlock relativeTo;

115
tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
@ -17,13 +18,15 @@ namespace Avalonia.Markup.UnitTests.Data
public class BindingExpressionTests : IClassFixture<InvariantCultureFixture>
{
[Fact]
public async void Should_Get_Simple_Property_Value()
public async Task Should_Get_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal("foo", result);
GC.KeepAlive(data);
}
[Fact]
@ -35,6 +38,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext("bar");
Assert.Equal("bar", data.StringValue);
GC.KeepAlive(data);
}
[Fact]
@ -46,102 +51,87 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext("bar");
Assert.Equal("bar", data.Foo[0]);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Convert_Get_String_To_Double()
public async Task Should_Convert_Get_String_To_Double()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = "5.6" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(5.6, result);
GC.KeepAlive(data);
}
[Fact]
public async void Getting_Invalid_Double_String_Should_Return_BindingError()
public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{
var data = new Class1 { StringValue = null };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Convert_Set_String_To_Double()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = (5.6).ToString() };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
target.OnNext(6.7);
Assert.Equal((6.7).ToString(), data.StringValue);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Convert_Get_Double_To_String()
public async Task Should_Convert_Get_Double_To_String()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal((5.6).ToString(), result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Convert_Set_Double_To_String()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("6.7");
Assert.Equal(6.7, data.DoubleValue);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value()
public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
@ -156,17 +146,13 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error,
42),
result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation()
public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
@ -181,17 +167,13 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error,
42),
result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
@ -203,21 +185,17 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(
new BindingNotification(
new AggregateException(
new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
new InvalidCastException("'foo' is not a valid number."),
new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
{
#if NET461
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
#else
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
#endif
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
@ -229,10 +207,12 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(
new BindingNotification(
new AggregateException(
new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
new InvalidCastException("'foo' is not a valid number."),
new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
@ -244,6 +224,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext("foo");
Assert.Equal(5.6, data.DoubleValue);
GC.KeepAlive(data);
}
[Fact]
@ -259,6 +241,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext("foo");
Assert.Equal(9.8, data.DoubleValue);
GC.KeepAlive(data);
}
[Fact]
@ -270,6 +254,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext(null);
Assert.Equal(0, data.DoubleValue);
GC.KeepAlive(data);
}
[Fact]
@ -281,6 +267,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(0, data.DoubleValue);
GC.KeepAlive(data);
}
[Fact]
@ -288,6 +276,7 @@ namespace Avalonia.Markup.UnitTests.Data
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
@ -296,7 +285,9 @@ namespace Avalonia.Markup.UnitTests.Data
target.Subscribe(_ => { });
converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentCulture));
GC.KeepAlive(data);
}
[Fact]
@ -312,7 +303,9 @@ namespace Avalonia.Markup.UnitTests.Data
target.OnNext("bar");
converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentCulture));
GC.KeepAlive(data);
}
[Fact]
@ -339,6 +332,8 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error)
},
result);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase

5
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Markup.Data;
using Xunit;
@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
public async void Should_Get_Attached_Property_Value()
public async Task Should_Get_Attached_Property_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "(Owner.Foo)");
@ -30,7 +31,7 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
public async void Should_Get_Chained_Attached_Property_Value()
public async Task Should_Get_Chained_Attached_Property_Value()
{
var data = new Class1
{

3
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Markup.Data;
using Xunit;
@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
public async void Should_Get_Simple_Property_Value()
public async Task Should_Get_Simple_Property_Value()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Foo");

9
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs

@ -28,6 +28,8 @@ namespace Avalonia.Markup.UnitTests.Data
observer.SetValue(-5);
Assert.False(validationMessageFound);
GC.KeepAlive(data);
}
[Fact]
@ -43,6 +45,8 @@ namespace Avalonia.Markup.UnitTests.Data
observer.SetValue(-5);
Assert.True(validationMessageFound);
GC.KeepAlive(data);
}
[Fact]
@ -102,6 +106,8 @@ namespace Avalonia.Markup.UnitTests.Data
new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
new BindingNotification(5),
}, result);
GC.KeepAlive(data);
}
[Fact]
@ -147,6 +153,9 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
}, result);
GC.KeepAlive(container);
GC.KeepAlive(inner);
}
public class ExceptionTest : NotifyingBase

67
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Diagnostics;
using Avalonia.Markup.Data;
@ -16,113 +17,135 @@ namespace Avalonia.Markup.UnitTests.Data
public class ExpressionObserverTests_Indexer
{
[Fact]
public async void Should_Get_Array_Value()
public async Task Should_Get_Array_Value()
{
var data = new { Foo = new [] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_UnsetValue_For_Invalid_Array_Index()
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_UnsetValue_For_Object_Without_Indexer()
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = new ExpressionObserver(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_MultiDimensional_Array_Value()
public async Task Should_Get_MultiDimensional_Array_Value()
{
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[1, 1]");
var result = await target.Take(1);
Assert.Equal("qux", result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_Value_For_String_Indexer()
public async Task Should_Get_Value_For_String_Indexer()
{
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[foo]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_Value_For_Non_String_Indexer()
public async Task Should_Get_Value_For_Non_String_Indexer()
{
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = new ExpressionObserver(data, "Foo[1.0]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
public async void Array_Out_Of_Bounds_Should_Return_UnsetValue()
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{
var data = new { Foo = new[] { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1,2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void List_Out_Of_Bounds_Should_Return_UnsetValue()
public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_List_Value()
public async Task Should_Get_List_Value()
{
var data = new { Foo = new List<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = await target.Take(1);
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
[Fact]
@ -139,6 +162,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
@ -155,6 +180,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
@ -171,6 +198,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
GC.KeepAlive(data);
}
[Fact]
@ -187,6 +216,9 @@ namespace Avalonia.Markup.UnitTests.Data
data.Foo.Move(0, 1);
Assert.Equal(new[] { "bar", "foo" }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
@ -200,6 +232,9 @@ namespace Avalonia.Markup.UnitTests.Data
data.Foo.Clear();
Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result);
GC.KeepAlive(sub);
GC.KeepAlive(data);
}
[Fact]
@ -220,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -234,6 +271,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal("baz", data.Foo[1]);
GC.KeepAlive(data);
}
[Fact]
@ -254,6 +293,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal(4, data.Foo["foo"]);
GC.KeepAlive(data);
}
[Fact]
@ -274,6 +315,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal(4, data.Foo["bar"]);
GC.KeepAlive(data);
}
[Fact]
@ -291,6 +334,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal("bar2", data.Foo["foo"]);
GC.KeepAlive(data);
}
private class NonIntegerIndexer : NotifyingBase

5
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs

@ -90,7 +90,8 @@ namespace Avalonia.Markup.UnitTests.Data
{
var scheduler = new TestScheduler();
var update = scheduler.CreateColdObservable<Unit>();
var target = new ExpressionObserver(() => new { Foo = "foo" }, "Foo", update);
var data = new { Foo = "foo" };
var target = new ExpressionObserver(() => data, "Foo", update);
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
@ -101,6 +102,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(new[] { "foo" }, result);
Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
GC.KeepAlive(data);
}
private Recorded<Notification<object>> OnNext(long time, object value)

31
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs

@ -3,6 +3,7 @@
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
@ -12,57 +13,67 @@ namespace Avalonia.Markup.UnitTests.Data
public class ExpressionObserverTests_Negation
{
[Fact]
public async void Should_Negate_Boolean_Value()
public async Task Should_Negate_Boolean_Value()
{
var data = new { Foo = true };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(false, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Negate_0()
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(true, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Negate_1()
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(false, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Negate_False_String()
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(true, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Negate_True_String()
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(false, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
@ -73,10 +84,12 @@ namespace Avalonia.Markup.UnitTests.Data
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
@ -87,6 +100,8 @@ namespace Avalonia.Markup.UnitTests.Data
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
@ -96,6 +111,8 @@ namespace Avalonia.Markup.UnitTests.Data
var target = new ExpressionObserver(data, "!Foo");
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
}
}

12
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs

@ -29,6 +29,8 @@ namespace Avalonia.Markup.UnitTests.Data
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { source }, result);
GC.KeepAlive(data);
}
}
@ -47,6 +49,8 @@ namespace Avalonia.Markup.UnitTests.Data
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { "foo", "bar" }, result);
GC.KeepAlive(data);
}
}
@ -67,6 +71,8 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
}
@ -87,6 +93,8 @@ namespace Avalonia.Markup.UnitTests.Data
// What does it mean to have data validation on an observable? Without a use-case
// it's hard to know what to do here so for the moment the value is returned.
Assert.Equal(new[] { "foo", "bar" }, result);
GC.KeepAlive(data);
}
}
@ -107,6 +115,8 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
}
@ -132,6 +142,8 @@ namespace Avalonia.Markup.UnitTests.Data
result);
sub.Dispose();
GC.KeepAlive(data);
}
}

78
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -11,19 +11,22 @@ using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
using System.Threading.Tasks;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_Property
{
[Fact]
public async void Should_Get_Simple_Property_Value()
public async Task Should_Get_Simple_Property_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.Equal("foo", result);
GC.KeepAlive(data);
}
[Fact]
@ -35,76 +38,92 @@ namespace Avalonia.Markup.UnitTests.Data
target.Subscribe(_ => { });
Assert.Equal(typeof(string), target.ResultType);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_Simple_Property_Value_Null()
public async Task Should_Get_Simple_Property_Value_Null()
{
var data = new { Foo = (string)null };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.Null(result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_Simple_Property_From_Base_Class()
public async Task Should_Get_Simple_Property_From_Base_Class()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.Equal("foo", result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_UnsetValue_For_Root_Null()
public async Task Should_Return_UnsetValue_For_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(default(object), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_UnsetValue_For_Root_UnsetValue()
public async Task Should_Return_UnsetValue_For_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_UnsetValue_For_Observable_Root_Null()
public async Task Should_Return_UnsetValue_For_Observable_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue()
public async Task Should_Return_UnsetValue_For_Observable_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Get_Simple_Property_Chain()
public async Task Should_Get_Simple_Property_Chain()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.Equal("baz", result);
GC.KeepAlive(data);
}
[Fact]
@ -116,10 +135,12 @@ namespace Avalonia.Markup.UnitTests.Data
target.Subscribe(_ => { });
Assert.Equal(typeof(string), target.ResultType);
GC.KeepAlive(data);
}
[Fact]
public async void Should_Return_BindingNotification_Error_For_Broken_Chain()
public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
@ -131,6 +152,8 @@ namespace Avalonia.Markup.UnitTests.Data
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
@ -151,6 +174,8 @@ namespace Avalonia.Markup.UnitTests.Data
AvaloniaProperty.UnsetValue),
},
result);
GC.KeepAlive(data);
}
[Fact]
@ -160,6 +185,8 @@ namespace Avalonia.Markup.UnitTests.Data
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
GC.KeepAlive(data);
}
[Fact]
@ -177,6 +204,8 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -205,6 +234,8 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -224,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -245,6 +278,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -286,6 +321,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -318,6 +355,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -334,6 +373,8 @@ namespace Avalonia.Markup.UnitTests.Data
update.OnNext(Unit.Default);
Assert.Equal(new[] { "foo", "bar" }, result);
GC.KeepAlive(data);
}
[Fact]
@ -374,6 +415,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(new[] { "foo", "bar" }, result1);
Assert.Equal(new[] { "foo", "bar" }, result2);
Assert.Equal(new[] { "bar" }, result3);
GC.KeepAlive(data);
}
[Fact]
@ -391,6 +434,8 @@ namespace Avalonia.Markup.UnitTests.Data
sub2.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
GC.KeepAlive(data);
}
[Fact]
@ -405,6 +450,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal("bar", data.Foo);
GC.KeepAlive(data);
}
[Fact]
@ -419,6 +466,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
Assert.Equal("baz", ((Class2)data.Next).Bar);
GC.KeepAlive(data);
}
[Fact]
@ -431,6 +480,8 @@ namespace Avalonia.Markup.UnitTests.Data
{
Assert.False(target.SetValue("baz"));
}
GC.KeepAlive(data);
}
[Fact]
@ -444,6 +495,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.SetValue("bar");
Assert.Equal(new[] { null, "bar" }, result);
GC.KeepAlive(data);
}
[Fact]
@ -457,6 +510,8 @@ namespace Avalonia.Markup.UnitTests.Data
target.SetValue("bar");
Assert.Equal(new[] { null, "bar" }, result);
GC.KeepAlive(data);
}
[Fact]
@ -469,6 +524,8 @@ namespace Avalonia.Markup.UnitTests.Data
{
Assert.False(target.SetValue("baz"));
}
GC.KeepAlive(data);
}
[Fact]
@ -498,6 +555,9 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, first.PropertyChangedSubscriptionCount);
Assert.Equal(0, second.PropertyChangedSubscriptionCount);
GC.KeepAlive(first);
GC.KeepAlive(second);
}
[Fact]

12
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs

@ -30,6 +30,8 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(1, result.Count);
Assert.IsType<Task<string>>(result[0]);
GC.KeepAlive(data);
}
}
@ -45,6 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(new[] { "foo" }, result);
GC.KeepAlive(data);
}
}
@ -63,6 +67,8 @@ namespace Avalonia.Markup.UnitTests.Data
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { "foo" }, result);
GC.KeepAlive(data);
}
}
@ -88,6 +94,8 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error)
},
result);
GC.KeepAlive(data);
}
}
@ -110,6 +118,8 @@ namespace Avalonia.Markup.UnitTests.Data
BindingErrorType.Error)
},
result);
GC.KeepAlive(data);
}
}
@ -130,6 +140,8 @@ namespace Avalonia.Markup.UnitTests.Data
// What does it mean to have data validation on a Task? Without a use-case it's
// hard to know what to do here so for the moment the value is returned.
Assert.Equal(new [] { "foo" }, result);
GC.KeepAlive(data);
}
}

2
tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs

@ -35,6 +35,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
new BindingNotification(6),
}, result);
GC.KeepAlive(data);
}
public class Data : NotifyingBase

2
tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs

@ -37,4 +37,4 @@ using Xunit;
[assembly: AssemblyFileVersion("1.0.0.0")]
// Don't run tests in parallel.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: CollectionBehavior(MaxParallelThreads = 1)]

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

3
tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs

@ -10,13 +10,14 @@ using Moq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.Data;
using Xunit;
using System.Threading.Tasks;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
public class MultiBindingTests
{
[Fact]
public async void OneWay_Binding_Should_Be_Set_Up()
public async Task OneWay_Binding_Should_Be_Set_Up()
{
var source = new { A = 1, B = 2, C = 3 };
var binding = new MultiBinding

2
tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs

@ -7,4 +7,4 @@ using Xunit;
[assembly: AssemblyTitle("Avalonia.Markup.Xaml.UnitTests")]
// Don't run tests in parallel.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: CollectionBehavior(MaxParallelThreads = 1)]

1
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

8
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Data;
@ -45,7 +46,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public async void Child_Matches_Control_When_It_Is_Child_OfType_And_Class()
public async Task Child_Matches_Control_When_It_Is_Child_OfType_And_Class()
{
var parent = new TestLogical1();
var child = new TestLogical2();
@ -144,6 +145,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
throw new NotImplementedException();
}
public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
throw new NotImplementedException();

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@ -175,6 +175,11 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
throw new NotImplementedException();
}
public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
throw new NotImplementedException();

4
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -51,8 +52,5 @@
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\XUnit.props" />
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
</ItemGroup>
<Import Condition="'$(TargetFramework)' == 'net461'" Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
</Project>

15
tests/Avalonia.UnitTests/InvariantCultureFixture.cs

@ -20,13 +20,22 @@ namespace Avalonia.UnitTests
public InvariantCultureFixture()
{
_restore = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
#if NET461
_restore = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
#else
_restore = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
#endif
}
public void Dispose()
{
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = _restore;
#if NET461
Thread.CurrentThread.CurrentCulture = _restore;
#else
CultureInfo.CurrentCulture = _restore;
#endif
}
}
}

2
tests/Avalonia.UnitTests/TestRoot.cs

@ -43,7 +43,7 @@ namespace Avalonia.UnitTests
public Size ClientSize => new Size(100, 100);
public Size MaxClientSize => Size.Infinity;
public Size MaxClientSize { get; set; } = Size.Infinity;
public double LayoutScaling => 1;

6
tests/Avalonia.UnitTests/TestServices.cs

@ -50,6 +50,7 @@ namespace Avalonia.UnitTests
public static readonly TestServices RealFocus = new TestServices(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager());
public static readonly TestServices RealLayoutManager = new TestServices(
@ -63,6 +64,7 @@ namespace Avalonia.UnitTests
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func<IKeyboardDevice> keyboardDevice = null,
IKeyboardNavigationHandler keyboardNavigation = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
@ -79,6 +81,7 @@ namespace Avalonia.UnitTests
FocusManager = focusManager;
InputManager = inputManager;
KeyboardDevice = keyboardDevice;
KeyboardNavigation = keyboardNavigation;
LayoutManager = layoutManager;
Platform = platform;
Renderer = renderer;
@ -96,6 +99,7 @@ namespace Avalonia.UnitTests
public IInputManager InputManager { get; }
public IFocusManager FocusManager { get; }
public Func<IKeyboardDevice> KeyboardDevice { get; }
public IKeyboardNavigationHandler KeyboardNavigation { get; }
public ILayoutManager LayoutManager { get; }
public IRuntimePlatform Platform { get; }
public Func<IRenderRoot, IRenderLoop, IRenderer> Renderer { get; }
@ -113,6 +117,7 @@ namespace Avalonia.UnitTests
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func<IKeyboardDevice> keyboardDevice = null,
IKeyboardNavigationHandler keyboardNavigation = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
Func<IRenderRoot, IRenderLoop, IRenderer> renderer = null,
@ -131,6 +136,7 @@ namespace Avalonia.UnitTests
focusManager: focusManager ?? FocusManager,
inputManager: inputManager ?? InputManager,
keyboardDevice: keyboardDevice ?? KeyboardDevice,
keyboardNavigation: keyboardNavigation ?? KeyboardNavigation,
layoutManager: layoutManager ?? LayoutManager,
platform: platform ?? Platform,
renderer: renderer ?? Renderer,

1
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -49,6 +49,7 @@ namespace Avalonia.UnitTests
.BindToSelf<IGlobalStyles>(this)
.Bind<IInputManager>().ToConstant(Services.InputManager)
.Bind<IKeyboardDevice>().ToConstant(Services.KeyboardDevice?.Invoke())
.Bind<IKeyboardNavigationHandler>().ToConstant(Services.KeyboardNavigation)
.Bind<ILayoutManager>().ToConstant(Services.LayoutManager)
.Bind<IRuntimePlatform>().ToConstant(Services.Platform)
.Bind<IRendererFactory>().ToConstant(new RendererFactory(Services.Renderer))

4
tools/packages.config

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Cake" version="0.18.0" />
</packages>
Loading…
Cancel
Save