diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index dcb3ef4a2b..546133bb03 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -1,6 +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 System;
+using System.Runtime.InteropServices;
+
namespace Avalonia.Utilities
{
///
@@ -8,6 +11,86 @@ namespace Avalonia.Utilities
///
public static class MathUtilities
{
+ ///
+ /// AreClose - Returns whether or not two doubles are "close". That is, whether or
+ /// not they are within epsilon of each other.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ 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);
+ }
+
+ ///
+ /// 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.
+ /// The first double to compare.
+ /// The second double to compare.
+ public static bool LessThan(double value1, double value2)
+ {
+ return (value1 < value2) && !AreClose(value1, value2);
+ }
+
+ ///
+ /// 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.
+ /// The first double to compare.
+ /// The second double to compare.
+ public static bool GreaterThan(double value1, double value2)
+ {
+ return (value1 > value2) && !AreClose(value1, value2);
+ }
+
+ ///
+ /// 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.
+ /// The first double to compare.
+ /// The second double to compare.
+ public static bool LessThanOrClose(double value1, double value2)
+ {
+ return (value1 < value2) || AreClose(value1, value2);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ public static bool GreaterThanOrClose(double value1, double value2)
+ {
+ return (value1 > value2) || AreClose(value1, value2);
+ }
+
+ ///
+ /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
+ /// but this is faster.
+ ///
+ /// The double to compare to 1.
+ public static bool IsOne(double value)
+ {
+ return Math.Abs(value - 1.0) < 10.0 * double.Epsilon;
+ }
+
+ ///
+ /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
+ /// but this is faster.
+ ///
+ /// The double to compare to 0.
+ public static bool IsZero(double value)
+ {
+ return Math.Abs(value) < 10.0 * double.Epsilon;
+ }
+
///
/// Clamps a value between a minimum and maximum value.
///
@@ -31,6 +114,39 @@ namespace Avalonia.Utilities
}
}
+ ///
+ /// Calculates the value to be used for layout rounding at high DPI.
+ ///
+ /// Input value to be rounded.
+ /// Ratio of screen's DPI to layout DPI
+ /// Adjusted value that will produce layout rounding on screen at high dpi.
+ /// 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.
+ 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;
+ }
+
///
/// Clamps a value between a minimum and maximum value.
///
@@ -54,4 +170,4 @@ namespace Avalonia.Utilities
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 597734d400..4df1b39400 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using Avalonia.Input;
+using Avalonia.Utilities;
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);
-
///
- protected override Size MeasureOverride(Size availableSize)
+ protected override Size MeasureOverride(Size constraint)
{
- var desiredSize = CreateUVSize();
- var lineSize = CreateUVSize();
- var uvAvailableSize = CreateUVSize(availableSize);
+ var curLineSize = new UVSize(Orientation);
+ var panelSize = new UVSize(Orientation);
+ 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 childSize = CreateUVSize(child.DesiredSize);
- if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line
+ var child = Children[i];
+ if (child == null) continue;
+
+ //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;
- lineSize.V = Max(lineSize.V, childSize.V);
+ panelSize.U = Max(curLineSize.U, panelSize.U);
+ 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);
- desiredSize.V += lineSize.V;
- lineSize = childSize;
+ curLineSize.U += sz.U;
+ curLineSize.V = Max(sz.V, curLineSize.V);
}
}
- // 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);
}
///
protected override Size ArrangeOverride(Size finalSize)
{
+ int firstInLine = 0;
double accumulatedV = 0;
- var uvFinalSize = CreateUVSize(finalSize);
- var lineSize = CreateUVSize();
- int firstChildInLineIndex = 0;
- for (int index = 0; index < Children.Count; index++)
+ UVSize curLineSize = new UVSize(Orientation);
+ UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
+
+ for (int i = 0; i < Children.Count; i++)
{
- var child = Children[index];
- var childSize = CreateUVSize(child.DesiredSize);
- if (lineSize.U + childSize.U <= uvFinalSize.U) // same line
+ var child = Children[i];
+ if (child == null) continue;
+
+ 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;
- lineSize.V = Max(lineSize.V, childSize.V);
+ arrangeLine(accumulatedV, curLineSize.V, firstInLine, i);
+
+ 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);
- ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
- accumulatedV += lineSize.V;
- lineSize = childSize;
- firstChildInLineIndex = index;
+ curLineSize.U += sz.U;
+ curLineSize.V = Max(sz.V, curLineSize.V);
}
}
- if (firstChildInLineIndex < Children.Count)
+ //arrange the last line, if any
+ if (firstInLine < Children.Count)
{
- var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count);
- ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
+ arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count);
}
- return finalSize;
- }
- private IEnumerable GetControlsBetween(int first, int last)
- {
- return Children.Skip(first).Take(last - first);
+ return finalSize;
}
- private void ArrangeLine(double v, double lineV, IEnumerable controls)
+ private void arrangeLine(double v, double lineV, int start, int end)
{
double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal);
- foreach (var child in controls)
+
+ for (int i = start; i < end; i++)
{
- var childSize = CreateUVSize(child.DesiredSize);
- var x = isHorizontal ? u : v;
- var y = isHorizontal ? v : u;
- var width = isHorizontal ? childSize.U : lineV;
- var height = isHorizontal ? lineV : childSize.U;
- child.Arrange(new Rect(x, y, width, height));
- u += childSize.U;
+ var child = Children[i];
+ if (child != null)
+ {
+ UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
+ double layoutSlotU = childSize.U;
+ child.Arrange(new Rect(
+ (isHorizontal ? u : v),
+ (isHorizontal ? v : u),
+ (isHorizontal ? layoutSlotU : lineV),
+ (isHorizontal ? lineV : layoutSlotU)));
+ u += layoutSlotU;
+ }
}
}
- ///
- /// 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)
- ///
- [DebuggerDisplay("U = {U} V = {V}")]
+
private struct UVSize
{
- private readonly Orientation _orientation;
-
- internal double U;
-
- internal double V;
-
- private UVSize(Orientation orientation, double width, double height)
+ internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
@@ -202,52 +221,25 @@ namespace Avalonia.Controls
Height = height;
}
- internal UVSize(Orientation orientation, Size size)
- : this(orientation, size.Width, size.Height)
- {
- }
-
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
- private double Width
+ internal double U;
+ internal double V;
+ private Orientation _orientation;
+
+ internal double Width
{
get { return (_orientation == Orientation.Horizontal ? U : V); }
- set
- {
- if (_orientation == Orientation.Horizontal)
- {
- U = value;
- }
- else
- {
- V = value;
- }
- }
+ set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
-
- private double Height
+ internal double Height
{
get { return (_orientation == Orientation.Horizontal ? V : U); }
- set
- {
- if (_orientation == Orientation.Horizontal)
- {
- V = value;
- }
- else
- {
- U = value;
- }
- }
- }
-
- public Size ToSize()
- {
- return new Size(Width, Height);
+ set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
}
diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
index f67cb7f40a..ced26a3004 100644
--- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
+++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
@@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI
return builder.AfterSetup(_ =>
{
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
- Locator.CurrentMutable.Register(
- () => new AvaloniaActivationForViewFetcher(),
- typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
});
}
}
diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
new file mode 100644
index 0000000000..3f41f54363
--- /dev/null
+++ b/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
+{
+ ///
+ /// 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.
+ ///
+ public class AutoDataTemplateBindingHook : IPropertyBindingHook
+ {
+ private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate
public class CrossFade : IPageTransition
{
- private Animation _fadeOutAnimation;
- private Animation _fadeInAnimation;
+ private readonly Animation _fadeOutAnimation;
+ private readonly Animation _fadeInAnimation;
///
/// Initializes a new instance of the class.
@@ -61,10 +61,10 @@ namespace Avalonia.Animation
new Setter
{
Property = Visual.OpacityProperty,
- Value = 0d
+ Value = 1d
}
},
- Cue = new Cue(0d)
+ Cue = new Cue(1d)
}
}
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
new file mode 100644
index 0000000000..a79647d355
--- /dev/null
+++ b/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)]
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
new file mode 100644
index 0000000000..667462eb91
--- /dev/null
+++ b/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 { }
+
+ public class ExampleViewModel : ReactiveObject
+ {
+ public ObservableCollection Items { get; } = new ObservableCollection();
+ }
+
+ public class ExampleView : ReactiveUserControl
+ {
+ 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));
+ }
+
+ [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(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(host.ViewModel);
+ Assert.IsType(host.DataContext);
+
+ host.DataContext = "changed context";
+ Assert.IsType(host.ViewModel);
+ Assert.IsType(host.DataContext);
+ }
+
+ private FuncControlTemplate GetTemplate()
+ {
+ return new FuncControlTemplate(parent =>
+ {
+ return new Border
+ {
+ Background = new Media.SolidColorBrush(0xffffffff),
+ Child = new ItemsPresenter
+ {
+ Name = "PART_ItemsPresenter",
+ MemberSelector = parent.MemberSelector,
+ [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
+ }
+ };
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
index d9f1ce47dd..2c81e8fea3 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
+++ b/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.Reactive.Concurrency;
using System.Reactive.Disposables;
@@ -13,7 +16,7 @@ using Splat;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Avalonia
+namespace Avalonia.ReactiveUI.UnitTests
{
public class AvaloniaActivationForViewFetcherTest
{
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
new file mode 100644
index 0000000000..328b749ba1
--- /dev/null
+++ b/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 { }
+
+ [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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs
new file mode 100644
index 0000000000..ff77de8d28
--- /dev/null
+++ b/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 { }
+
+ [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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
index 401d169896..8c5b5083e8 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
+++ b/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.Reactive.Concurrency;
using System.Reactive.Disposables;
@@ -16,7 +19,7 @@ using System.Threading.Tasks;
using System.Reactive;
using Avalonia.ReactiveUI;
-namespace Avalonia
+namespace Avalonia.ReactiveUI.UnitTests
{
public class RoutedViewHostTest
{
@@ -59,8 +62,7 @@ namespace Avalonia
{
Router = screen.Router,
DefaultContent = defaultContent,
- FadeOutAnimation = null,
- FadeInAnimation = null
+ PageTransition = null
};
var root = new TestRoot
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs
new file mode 100644
index 0000000000..f09eea5ec5
--- /dev/null
+++ b/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(child);
+ child = child.VisualChildren.Single();
+ Assert.IsType(child);
+ child = child.VisualChildren.Single();
+ Assert.IsType(child);
+ }
+
+ private FuncControlTemplate GetTemplate()
+ {
+ return new FuncControlTemplate(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],
+ }
+ };
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs
new file mode 100644
index 0000000000..8bed5adcd4
--- /dev/null
+++ b/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 { }
+
+ public class SecondViewModel : ReactiveObject { }
+
+ public class SecondView : ReactiveUserControl { }
+
+ public ViewModelViewHostTest()
+ {
+ Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor));
+ Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor));
+ }
+
+ [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);
+ }
+ }
+}
\ No newline at end of file