Browse Source

Merge branch 'master' into win32-jitter

pull/2606/head
Wiesław Šoltés 7 years ago
committed by GitHub
parent
commit
92aefce3a8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 118
      src/Avalonia.Base/Utilities/MathUtilities.cs
  2. 196
      src/Avalonia.Controls/WrapPanel.cs
  3. 5
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  4. 55
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  5. 143
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  6. 75
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  7. 80
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  8. 8
      src/Avalonia.Visuals/Animation/CrossFade.cs
  9. 9
      tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
  10. 116
      tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
  11. 5
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  12. 34
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
  13. 37
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs
  14. 8
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  15. 63
      tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs
  16. 74
      tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

118
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -1,6 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
/// <summary> /// <summary>
@ -8,6 +11,86 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
public static class MathUtilities public static class MathUtilities
{ {
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
/// the other number.
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThan(double value1, double value2)
{
return (value1 < value2) && !AreClose(value1, value2);
}
/// <summary>
/// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
/// the other number.
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThan(double value1, double value2)
{
return (value1 > value2) && !AreClose(value1, value2);
}
/// <summary>
/// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within
/// epsilon of the other number.
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThanOrClose(double value1, double value2)
{
return (value1 < value2) || AreClose(value1, value2);
}
/// <summary>
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within
/// epsilon of the other number.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThanOrClose(double value1, double value2)
{
return (value1 > value2) || AreClose(value1, value2);
}
/// <summary>
/// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
/// but this is faster.
/// </summary>
/// <param name="value"> The double to compare to 1. </param>
public static bool IsOne(double value)
{
return Math.Abs(value - 1.0) < 10.0 * double.Epsilon;
}
/// <summary>
/// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
/// but this is faster.
/// </summary>
/// <param name="value"> The double to compare to 0. </param>
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0 * double.Epsilon;
}
/// <summary> /// <summary>
/// Clamps a value between a minimum and maximum value. /// Clamps a value between a minimum and maximum value.
/// </summary> /// </summary>
@ -31,6 +114,39 @@ namespace Avalonia.Utilities
} }
} }
/// <summary>
/// Calculates the value to be used for layout rounding at high DPI.
/// </summary>
/// <param name="value">Input value to be rounded.</param>
/// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
/// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
/// <remarks>This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with
/// UseLayoutRounding property and should not be used as a general rounding utility.</remarks>
public static double RoundLayoutValue(double value, double dpiScale)
{
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.AreClose(dpiScale, 1.0))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
}
/// <summary> /// <summary>
/// Clamps a value between a minimum and maximum value. /// Clamps a value between a minimum and maximum value.
/// </summary> /// </summary>
@ -54,4 +170,4 @@ namespace Avalonia.Utilities
} }
} }
} }
} }

196
src/Avalonia.Controls/WrapPanel.cs

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Utilities;
using static System.Math; using static System.Math;
@ -92,109 +93,127 @@ namespace Avalonia.Controls
} }
} }
private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size);
private UVSize CreateUVSize() => new UVSize(Orientation);
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size constraint)
{ {
var desiredSize = CreateUVSize(); var curLineSize = new UVSize(Orientation);
var lineSize = CreateUVSize(); var panelSize = new UVSize(Orientation);
var uvAvailableSize = CreateUVSize(availableSize); var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
foreach (var child in Children) var childConstraint = new Size(constraint.Width, constraint.Height);
for (int i = 0, count = Children.Count; i < count; i++)
{ {
child.Measure(availableSize); var child = Children[i];
var childSize = CreateUVSize(child.DesiredSize); if (child == null) continue;
if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line
//Flow passes its own constrint to children
child.Measure(childConstraint);
//this is the size of the child in UV space
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line
{ {
lineSize.U += childSize.U; panelSize.U = Max(curLineSize.U, panelSize.U);
lineSize.V = Max(lineSize.V, childSize.V); panelSize.V += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line
{
panelSize.U = Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(Orientation);
}
} }
else // moving to next line else //continue to accumulate a line
{ {
desiredSize.U = Max(lineSize.U, uvAvailableSize.U); curLineSize.U += sz.U;
desiredSize.V += lineSize.V; curLineSize.V = Max(sz.V, curLineSize.V);
lineSize = childSize;
} }
} }
// last element
desiredSize.U = Max(lineSize.U, desiredSize.U);
desiredSize.V += lineSize.V;
return desiredSize.ToSize(); //the last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
//go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
int firstInLine = 0;
double accumulatedV = 0; double accumulatedV = 0;
var uvFinalSize = CreateUVSize(finalSize); UVSize curLineSize = new UVSize(Orientation);
var lineSize = CreateUVSize(); UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
int firstChildInLineIndex = 0;
for (int index = 0; index < Children.Count; index++) for (int i = 0; i < Children.Count; i++)
{ {
var child = Children[index]; var child = Children[i];
var childSize = CreateUVSize(child.DesiredSize); if (child == null) continue;
if (lineSize.U + childSize.U <= uvFinalSize.U) // same line
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line
{ {
lineSize.U += childSize.U; arrangeLine(accumulatedV, curLineSize.V, firstInLine, i);
lineSize.V = Max(lineSize.V, childSize.V);
accumulatedV += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line
{
//switch to next line which only contain one element
arrangeLine(accumulatedV, sz.V, i, ++i);
accumulatedV += sz.V;
curLineSize = new UVSize(Orientation);
}
firstInLine = i;
} }
else // moving to next line else //continue to accumulate a line
{ {
var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); curLineSize.U += sz.U;
ArrangeLine(accumulatedV, lineSize.V, controlsInLine); curLineSize.V = Max(sz.V, curLineSize.V);
accumulatedV += lineSize.V;
lineSize = childSize;
firstChildInLineIndex = index;
} }
} }
if (firstChildInLineIndex < Children.Count) //arrange the last line, if any
if (firstInLine < Children.Count)
{ {
var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count);
ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
} }
return finalSize;
}
private IEnumerable<IControl> GetControlsBetween(int first, int last) return finalSize;
{
return Children.Skip(first).Take(last - first);
} }
private void ArrangeLine(double v, double lineV, IEnumerable<IControl> controls) private void arrangeLine(double v, double lineV, int start, int end)
{ {
double u = 0; double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal); bool isHorizontal = (Orientation == Orientation.Horizontal);
foreach (var child in controls)
for (int i = start; i < end; i++)
{ {
var childSize = CreateUVSize(child.DesiredSize); var child = Children[i];
var x = isHorizontal ? u : v; if (child != null)
var y = isHorizontal ? v : u; {
var width = isHorizontal ? childSize.U : lineV; UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
var height = isHorizontal ? lineV : childSize.U; double layoutSlotU = childSize.U;
child.Arrange(new Rect(x, y, width, height)); child.Arrange(new Rect(
u += childSize.U; (isHorizontal ? u : v),
(isHorizontal ? v : u),
(isHorizontal ? layoutSlotU : lineV),
(isHorizontal ? lineV : layoutSlotU)));
u += layoutSlotU;
}
} }
} }
/// <summary>
/// Used to not not write separate code for horizontal and vertical orientation.
/// U is direction in line. (x if orientation is horizontal)
/// V is direction of lines. (y if orientation is horizontal)
/// </summary>
[DebuggerDisplay("U = {U} V = {V}")]
private struct UVSize private struct UVSize
{ {
private readonly Orientation _orientation; internal UVSize(Orientation orientation, double width, double height)
internal double U;
internal double V;
private UVSize(Orientation orientation, double width, double height)
{ {
U = V = 0d; U = V = 0d;
_orientation = orientation; _orientation = orientation;
@ -202,52 +221,25 @@ namespace Avalonia.Controls
Height = height; Height = height;
} }
internal UVSize(Orientation orientation, Size size)
: this(orientation, size.Width, size.Height)
{
}
internal UVSize(Orientation orientation) internal UVSize(Orientation orientation)
{ {
U = V = 0d; U = V = 0d;
_orientation = orientation; _orientation = orientation;
} }
private double Width internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{ {
get { return (_orientation == Orientation.Horizontal ? U : V); } get { return (_orientation == Orientation.Horizontal ? U : V); }
set set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
{
if (_orientation == Orientation.Horizontal)
{
U = value;
}
else
{
V = value;
}
}
} }
internal double Height
private double Height
{ {
get { return (_orientation == Orientation.Horizontal ? V : U); } get { return (_orientation == Orientation.Horizontal ? V : U); }
set set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
{
if (_orientation == Orientation.Horizontal)
{
V = value;
}
else
{
U = value;
}
}
}
public Size ToSize()
{
return new Size(Width, Height);
} }
} }
} }

5
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI
return builder.AfterSetup(_ => return builder.AfterSetup(_ =>
{ {
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.Register( Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
() => new AvaloniaActivationForViewFetcher(), Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
typeof(IActivationForViewFetcher));
}); });
} }
} }

55
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@ -0,0 +1,55 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Templates;
using ReactiveUI;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls
/// that don't have DataTemplates, and assigns a default DataTemplate that
/// loads the View associated with each ViewModel.
/// </summary>
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>(x =>
{
var control = new ViewModelViewHost();
var context = control.GetObservable(Control.DataContextProperty);
control.Bind(ViewModelViewHost.ViewModelProperty, context);
control.HorizontalContentAlignment = HorizontalAlignment.Stretch;
control.VerticalContentAlignment = VerticalAlignment.Stretch;
return control;
},
true);
/// <inheritdoc/>
public bool ExecuteHook(
object source, object target,
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
Func<IObservedChange<object, object>[]> getCurrentViewProperties,
BindingDirection direction)
{
var viewProperties = getCurrentViewProperties();
var lastViewProperty = viewProperties.LastOrDefault();
var itemsControl = lastViewProperty?.Sender as ItemsControl;
if (itemsControl == null)
return true;
var propertyName = viewProperties.Last().GetPropertyName();
if (propertyName != "Items" &&
propertyName != "ItemsSource")
return true;
if (itemsControl.ItemTemplate != null)
return true;
itemsControl.ItemTemplate = DefaultItemTemplate;
return true;
}
}
}

143
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI
/// ReactiveUI routing documentation website</see> for more info. /// ReactiveUI routing documentation website</see> for more info.
/// </para> /// </para>
/// </remarks> /// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger
{ {
/// <summary> /// <summary>
/// The router dependency property. /// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
/// </summary> /// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty = public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router)); AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class. /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI
set => SetValue(RouterProperty, value); set => SetValue(RouterProperty, value);
} }
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary> /// <summary>
/// Gets or sets the ReactiveUI view locator used by this router. /// Gets or sets the ReactiveUI view locator used by this router.
/// </summary> /// </summary>
@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception"> private void NavigateToViewModel(object viewModel)
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
private void NavigateToViewModel(IRoutableViewModel viewModel)
{ {
if (viewModel == null) if (viewModel == null)
{ {
this.Log().Info("ViewModel is null, falling back to default content."); this.Log().Info("ViewModel is null. Falling back to default content.");
UpdateContent(DefaultContent); Content = DefaultContent;
return; return;
} }
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(viewModel); var viewInstance = viewLocator.ResolveView(viewModel);
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {view} with autowired {viewModel}."); this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
view.ViewModel = viewModel; viewInstance.ViewModel = viewModel;
if (view is IStyledElement styled) if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel; styled.DataContext = viewModel;
UpdateContent(view); Content = viewInstance;
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
{
return new Avalonia.Animation.Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = from
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = to
}
},
Cue = new Cue(1d)
}
}
};
} }
} }
} }

75
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@ -0,0 +1,75 @@
// 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.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ContentControl that animates the transition when its content is changed.
/// </summary>
public class TransitioningContentControl : ContentControl, IStyleable
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
/// </summary>
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty =
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition),
new CrossFade(TimeSpan.FromSeconds(0.5)));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent));
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
public IPageTransition PageTransition
{
get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the content with animation.
/// </summary>
public new object Content
{
get => base.Content;
set => UpdateContentWithTransition(value);
}
/// <summary>
/// TransitioningContentControl uses the default ContentControl
/// template from Avalonia default theme.
/// </summary>
Type IStyleable.StyleKey => typeof(ContentControl);
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object content)
{
if (PageTransition != null)
await PageTransition.Start(this, null, true);
base.Content = content;
if (PageTransition != null)
await PageTransition.Start(null, this, true);
}
}
}

80
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -0,0 +1,80 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using ReactiveUI;
using Splat;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// This content control will automatically load the View associated with
/// the ViewModel property and display it. This control is very useful
/// inside a DataTemplate to display the View associated with a ViewModel.
/// </summary>
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel));
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary>
public ViewModelViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel)
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the ViewModel to display.
/// </summary>
public object ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// Gets or sets the view locator.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null. Falling back to default content.");
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;
Content = viewInstance;
}
}
}

8
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -14,8 +14,8 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class CrossFade : IPageTransition public class CrossFade : IPageTransition
{ {
private Animation _fadeOutAnimation; private readonly Animation _fadeOutAnimation;
private Animation _fadeInAnimation; private readonly Animation _fadeInAnimation;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> class. /// Initializes a new instance of the <see cref="CrossFade"/> class.
@ -61,10 +61,10 @@ namespace Avalonia.Animation
new Setter new Setter
{ {
Property = Visual.OpacityProperty, Property = Visual.OpacityProperty,
Value = 0d Value = 1d
} }
}, },
Cue = new Cue(0d) Cue = new Cue(1d)
} }
} }

9
tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs

@ -0,0 +1,9 @@
// 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 Xunit;
// Required to avoid InvalidOperationException sometimes thrown
// from Splat.MemoizingMRUCache.cs which is not thread-safe.
// Thrown when trying to access WhenActivated concurrently.
[assembly: CollectionBehavior(DisableTestParallelization = true)]

116
tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs

@ -0,0 +1,116 @@
// 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 Xunit;
using ReactiveUI;
using Avalonia.ReactiveUI;
using Avalonia.UnitTests;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.VisualTree;
using Avalonia.Controls.Presenters;
using Splat;
using System.Threading.Tasks;
using System;
namespace Avalonia.ReactiveUI.UnitTests
{
public class AutoDataTemplateBindingHookTest
{
public class NestedViewModel : ReactiveObject { }
public class NestedView : ReactiveUserControl<NestedViewModel> { }
public class ExampleViewModel : ReactiveObject
{
public ObservableCollection<NestedViewModel> Items { get; } = new ObservableCollection<NestedViewModel>();
}
public class ExampleView : ReactiveUserControl<ExampleViewModel>
{
public ItemsControl List { get; } = new ItemsControl();
public ExampleView()
{
Content = List;
ViewModel = new ExampleViewModel();
this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items);
}
}
public AutoDataTemplateBindingHookTest()
{
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor<NestedViewModel>));
}
[Fact]
public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set()
{
var view = new ExampleView();
Assert.NotNull(view.List.ItemTemplate);
}
[Fact]
public void Should_Use_View_Model_View_Host_As_Data_Template()
{
var view = new ExampleView();
view.ViewModel.Items.Add(new NestedViewModel());
view.List.Template = GetTemplate();
view.List.ApplyTemplate();
view.List.Presenter.ApplyTemplate();
var child = view.List.Presenter.Panel.Children[0];
var container = (ContentPresenter) child;
container.UpdateChild();
Assert.IsType<ViewModelViewHost>(container.Child);
}
[Fact]
public void Should_Resolve_And_Embedd_Appropriate_View_Model()
{
var view = new ExampleView();
var root = new TestRoot { Child = view };
view.ViewModel.Items.Add(new NestedViewModel());
view.List.Template = GetTemplate();
view.List.ApplyTemplate();
view.List.Presenter.ApplyTemplate();
var child = view.List.Presenter.Panel.Children[0];
var container = (ContentPresenter) child;
container.UpdateChild();
var host = (ViewModelViewHost) container.Child;
Assert.IsType<NestedViewModel>(host.ViewModel);
Assert.IsType<NestedViewModel>(host.DataContext);
host.DataContext = "changed context";
Assert.IsType<string>(host.ViewModel);
Assert.IsType<string>(host.DataContext);
}
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate<ItemsControl>(parent =>
{
return new Border
{
Background = new Media.SolidColorBrush(0xffffffff),
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
MemberSelector = parent.MemberSelector,
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}
};
});
}
}
}

5
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Reactive.Disposables; using System.Reactive.Disposables;
@ -13,7 +16,7 @@ using Splat;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Avalonia namespace Avalonia.ReactiveUI.UnitTests
{ {
public class AvaloniaActivationForViewFetcherTest public class AvaloniaActivationForViewFetcherTest
{ {

34
tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs

@ -0,0 +1,34 @@
// 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 Avalonia.UnitTests;
using ReactiveUI;
using Splat;
using Xunit;
namespace Avalonia.ReactiveUI.UnitTests
{
public class ReactiveUserControlTest
{
public class ExampleViewModel : ReactiveObject { }
public class ExampleView : ReactiveUserControl<ExampleViewModel> { }
[Fact]
public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model()
{
var view = new ExampleView();
var viewModel = new ExampleViewModel();
Assert.Null(view.ViewModel);
view.DataContext = viewModel;
Assert.Equal(view.ViewModel, viewModel);
Assert.Equal(view.DataContext, viewModel);
view.DataContext = null;
Assert.Null(view.ViewModel);
Assert.Null(view.DataContext);
}
}
}

37
tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs

@ -0,0 +1,37 @@
// 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 Avalonia.UnitTests;
using ReactiveUI;
using Splat;
using Xunit;
namespace Avalonia.ReactiveUI.UnitTests
{
public class ReactiveWindowTest
{
public class ExampleViewModel : ReactiveObject { }
public class ExampleWindow : ReactiveWindow<ExampleViewModel> { }
[Fact]
public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var view = new ExampleWindow();
var viewModel = new ExampleViewModel();
Assert.Null(view.ViewModel);
view.DataContext = viewModel;
Assert.Equal(view.ViewModel, viewModel);
Assert.Equal(view.DataContext, viewModel);
view.DataContext = null;
Assert.Null(view.ViewModel);
Assert.Null(view.DataContext);
}
}
}
}

8
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Reactive.Disposables; using System.Reactive.Disposables;
@ -16,7 +19,7 @@ using System.Threading.Tasks;
using System.Reactive; using System.Reactive;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Avalonia namespace Avalonia.ReactiveUI.UnitTests
{ {
public class RoutedViewHostTest public class RoutedViewHostTest
{ {
@ -59,8 +62,7 @@ namespace Avalonia
{ {
Router = screen.Router, Router = screen.Router,
DefaultContent = defaultContent, DefaultContent = defaultContent,
FadeOutAnimation = null, PageTransition = null
FadeInAnimation = null
}; };
var root = new TestRoot var root = new TestRoot

63
tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs

@ -0,0 +1,63 @@
// 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;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using ReactiveUI;
using Splat;
using Xunit;
namespace Avalonia.ReactiveUI.UnitTests
{
public class TransitioningContentControlTest
{
[Fact]
public void Transitioning_Control_Should_Derive_Template_From_Content_Control()
{
var target = new TransitioningContentControl();
var stylable = (IStyledElement)target;
Assert.Equal(typeof(ContentControl),stylable.StyleKey);
}
[Fact]
public void Transitioning_Control_Template_Should_Be_Instantiated()
{
var target = new TransitioningContentControl
{
PageTransition = null,
Template = GetTemplate(),
Content = "Foo"
};
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
var child = ((IVisual)target).VisualChildren.Single();
Assert.IsType<Border>(child);
child = child.VisualChildren.Single();
Assert.IsType<ContentPresenter>(child);
child = child.VisualChildren.Single();
Assert.IsType<TextBlock>(child);
}
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate<ContentControl>(parent =>
{
return new Border
{
Background = new Media.SolidColorBrush(0xffffffff),
Child = new ContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty],
[~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty],
}
};
});
}
}
}

74
tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

@ -0,0 +1,74 @@
// 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 Avalonia.UnitTests;
using ReactiveUI;
using Splat;
using Xunit;
namespace Avalonia.ReactiveUI.UnitTests
{
public class ViewModelViewHostTest
{
public class FirstViewModel { }
public class FirstView : ReactiveUserControl<FirstViewModel> { }
public class SecondViewModel : ReactiveObject { }
public class SecondView : ReactiveUserControl<SecondViewModel> { }
public ViewModelViewHostTest()
{
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>));
Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>));
}
[Fact]
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel()
{
var defaultContent = new TextBlock();
var host = new ViewModelViewHost
{
DefaultContent = defaultContent,
PageTransition = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
var first = new FirstViewModel();
host.ViewModel = first;
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstView), host.Content.GetType());
Assert.Equal(first, ((FirstView)host.Content).DataContext);
Assert.Equal(first, ((FirstView)host.Content).ViewModel);
var second = new SecondViewModel();
host.ViewModel = second;
Assert.NotNull(host.Content);
Assert.Equal(typeof(SecondView), host.Content.GetType());
Assert.Equal(second, ((SecondView)host.Content).DataContext);
Assert.Equal(second, ((SecondView)host.Content).ViewModel);
host.ViewModel = null;
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
host.ViewModel = first;
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstView), host.Content.GetType());
Assert.Equal(first, ((FirstView)host.Content).DataContext);
Assert.Equal(first, ((FirstView)host.Content).ViewModel);
}
}
}
Loading…
Cancel
Save