diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.Properties.cs index f9d2f3c0584..dc1cb62dfab 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.Properties.cs @@ -61,7 +61,7 @@ public int MultipleX /// Identifies the property. /// public static readonly DependencyProperty MultipleXProperty = - DependencyProperty.Register(nameof(MultipleX), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(MultipleX), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged)); /// /// Gets or sets the integer multiple that the height of the panel should be floored to. Default is null (no snap). @@ -76,7 +76,7 @@ public int MultipleY /// Identifies the property. /// public static readonly DependencyProperty MultipleYProperty = - DependencyProperty.Register(nameof(MultipleY), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(MultipleY), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged)); /// /// Gets or sets aspect Ratio to use for the contents of the Panel (after scaling). @@ -93,11 +93,71 @@ public AspectRatio AspectRatio public static readonly DependencyProperty AspectRatioProperty = DependencyProperty.Register(nameof(AspectRatio), typeof(AspectRatio), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged)); + private bool _propertyUpdating; + private static void ConstraintPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (d is ConstrainedBox panel) + if (d is ConstrainedBox panel && !panel._propertyUpdating) { + panel._propertyUpdating = true; + + panel.CoerceValues(); + panel.InvalidateMeasure(); + + panel._propertyUpdating = false; + } + } + + private void CoerceValues() + { + // Check if scale properties are in range + if (!double.IsNaN(ScaleX)) + { + if (ScaleX < 0) + { + ScaleX = 0; + } + else if (ScaleX > 1.0) + { + ScaleX = 1.0; + } + } + else + { + ScaleX = 1.0; + } + + if (!double.IsNaN(ScaleY)) + { + if (ScaleY < 0) + { + ScaleY = 0; + } + else if (ScaleY > 1.0) + { + ScaleY = 1.0; + } + } + else + { + ScaleY = 1.0; + } + + // Clear invalid values less than 0 for other properties + if (ReadLocalValue(MultipleXProperty) is int value && value <= 0) + { + ClearValue(MultipleXProperty); + } + + if (ReadLocalValue(MultipleYProperty) is int value2 && value2 <= 0) + { + ClearValue(MultipleYProperty); + } + + if (ReadLocalValue(AspectRatioProperty) is AspectRatio ratio && ratio <= 0) + { + ClearValue(AspectRatioProperty); } } } diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Alignment.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Alignment.cs new file mode 100644 index 00000000000..c5571a4c32e --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Alignment.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Markup; + +namespace UnitTests.UWP.UI.Controls +{ + /// + /// These tests check whether the inner alignment of the box within it's parent works as expected. + /// + public partial class Test_ConstrainedBox : VisualUITestBase + { + // For this test we're testing within the confines of a 200x200 box to position a contrained + // 50x100 element in all the different alignment combinations. + [TestCategory("ConstrainedBox")] + [TestMethod] + [DataRow("Left", 0, "Center", 50, DisplayName = "LeftCenter")] + [DataRow("Left", 0, "Top", 0, DisplayName = "LeftTop")] + [DataRow("Center", 75, "Top", 0, DisplayName = "CenterTop")] + [DataRow("Right", 150, "Top", 0, DisplayName = "RightTop")] + [DataRow("Right", 150, "Center", 50, DisplayName = "RightCenter")] + [DataRow("Right", 150, "Bottom", 100, DisplayName = "RightBottom")] + [DataRow("Center", 75, "Bottom", 100, DisplayName = "CenterBottom")] + [DataRow("Left", 0, "Bottom", 100, DisplayName = "LeftBottom")] + [DataRow("Center", 75, "Center", 50, DisplayName = "CenterCenter")] + public async Task Test_ConstrainedBox_Alignment_Aspect(string horizontalAlignment, int expectedLeft, string verticalAlignment, int expectedTop) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@$" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var grid = treeRoot.FindChild("ParentGrid") as Grid; + + Assert.IsNotNull(grid, "Could not find the ParentGrid in tree."); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(50, child.ActualWidth, 0.01, "Actual width does not meet expected value of 50"); + Assert.AreEqual(100, child.ActualHeight, 0.01, "Actual height does not meet expected value of 100"); + + // Check inner Positioning, we do this from the Grid as the ConstainedBox also modifies its own size + // and is hugging the child. + var position = grid.CoordinatesTo(child); + + Assert.AreEqual(expectedLeft, position.X, 0.01, "X position does not meet expected value of 0"); + Assert.AreEqual(expectedTop, position.Y, 0.01, "Y position does not meet expected value of 50"); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Coerce.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Coerce.cs new file mode 100644 index 00000000000..aaa69aac0d7 --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Coerce.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Markup; + +namespace UnitTests.UWP.UI.Controls +{ + /// + /// These tests check for the various values which can be coerced and changed if out of bounds for each property. + /// + public partial class Test_ConstrainedBox : VisualUITestBase + { + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Coerce_Scale() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Check Size + Assert.AreEqual(0.5, panel.ScaleX, 0.01, "ScaleX does not meet expected initial value of 0.5"); + Assert.AreEqual(0.5, panel.ScaleY, 0.01, "ScaleY does not meet expected initial value of 0.5"); + + // Change values now to be invalid + panel.ScaleX = double.NaN; + panel.ScaleY = double.NaN; + + Assert.AreEqual(1.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 1.0 after change."); + Assert.AreEqual(1.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 1.0 after change."); + + // Change values now to be invalid + panel.ScaleX = double.NegativeInfinity; + panel.ScaleY = double.NegativeInfinity; + + Assert.AreEqual(0.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 0.0 after change."); + Assert.AreEqual(0.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 0.0 after change."); + + // Change values now to be invalid + panel.ScaleX = double.PositiveInfinity; + panel.ScaleY = double.PositiveInfinity; + + Assert.AreEqual(1.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 1.0 after change."); + Assert.AreEqual(1.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 1.0 after change."); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Coerce_Multiple() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Check Size + Assert.AreEqual(32, panel.MultipleX, "MultipleX does not meet expected value of 32"); + Assert.AreEqual(32, panel.MultipleY, "MultipleY does not meet expected value of 32"); + + // Change values now to be invalid + panel.MultipleX = 0; + panel.MultipleY = int.MinValue; + + Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.MultipleXProperty), "MultipleX does not meet expected value of UnsetValue after change."); + Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.MultipleYProperty), "MultipleY does not meet expected value of UnsetValue after change."); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Coerce_AspectRatio() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Check Size + Assert.AreEqual(1.25 / 3.5, panel.AspectRatio, 0.01, "ApectRatio does not meet expected value of 1.25/3.5"); + + // Change values now to be invalid + panel.AspectRatio = -1; + + Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.AspectRatioProperty), "AspectRatio does not meet expected value of UnsetValue after change."); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Combined.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Combined.cs index 8c5c34f3090..997a38425d5 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Combined.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Combined.cs @@ -17,6 +17,9 @@ namespace UnitTests.UWP.UI.Controls { + /// + /// These tests check multiple constraints are applied together in the correct order. + /// public partial class Test_ConstrainedBox : VisualUITestBase { [TestCategory("ConstrainedBox")] diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Constrict.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Constrict.cs new file mode 100644 index 00000000000..1f14be50574 --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Constrict.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Markup; + +namespace UnitTests.UWP.UI.Controls +{ + /// + /// These tests check for cases where we want to constrain the size of the ConstrainedBox with other + /// properties like MaxWidth and MaxHeight. The ConstrainedBox is greedy about using all the available + /// space, so Min properties don't really play the same factor? At least hard to create a practical test. + /// + public partial class Test_ConstrainedBox : VisualUITestBase + { + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Constrain_AspectMaxHeight() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(200, child.ActualWidth, 0.01, "Actual width does not meet expected value of 200"); + Assert.AreEqual(100, child.ActualHeight, 0.01, "Actual height does not meet expected value of 100"); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Constrain_AspectMaxWidth() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(100, child.ActualWidth, 0.01, "Actual width does not meet expected value of 100"); + Assert.AreEqual(50, child.ActualHeight, 0.01, "Actual height does not meet expected value of 50"); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_Constrain_AspectBothMaxWidthHeight() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(100, child.ActualWidth, 0.01, "Actual width does not meet expected value of 100"); + Assert.AreEqual(200, child.ActualHeight, 0.01, "Actual height does not meet expected value of 200"); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Infinity.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Infinity.cs new file mode 100644 index 00000000000..012d19a7ed0 --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Infinity.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Markup; + +namespace UnitTests.UWP.UI.Controls +{ + /// + /// These tests check for cases where one of the bounds provided by the parent panel is infinite. + /// + public partial class Test_ConstrainedBox : VisualUITestBase + { + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_AllInfinite_AspectWidthFallback() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + // Even though we constrain the size of the ScrollViewer, it initially reports infinite measure + // which is what we want to test. We constrain the dimension so that we have a specific value + // that we can measure against. If we instead restrained the inner Border, it'd just set + // it's side within the constrained space of the parent layout... + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(200, child.ActualWidth, 0.01, "Actual width does not meet expected value of 200"); + Assert.AreEqual(100, child.ActualHeight, 0.01, "Actual height does not meet expected value of 100"); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_AllInfinite_AspectHeightFallback() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + // Check Size + Assert.AreEqual(100, child.ActualWidth, 0.01, "Actual width does not meet expected value of 100"); + Assert.AreEqual(200, child.ActualHeight, 0.01, "Actual height does not meet expected value of 200"); + }); + } + + [TestCategory("ConstrainedBox")] + [TestMethod] + public async Task Test_ConstrainedBox_AllInfinite_AspectBothFallback() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + +") as FrameworkElement; + + Assert.IsNotNull(treeRoot, "Could not load XAML tree."); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + var scroll = treeRoot.FindChild("ScrollArea") as ScrollViewer; + + Assert.IsNotNull(scroll, "Could not find ScrollViewer in tree."); + + var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox; + + Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree."); + + // Force Layout calculations + panel.UpdateLayout(); + + var child = panel.Content as Border; + + Assert.IsNotNull(child, "Could not find inner Border"); + + var width = scroll.ActualHeight / 2; + + // Check Size + Assert.AreEqual(width, child.ActualWidth, 0.01, "Actual width does not meet expected value of " + width); + Assert.AreEqual(scroll.ActualHeight, child.ActualHeight, 0.01, "Actual height does not meet expected value of " + scroll.ActualHeight); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index 734228e69b1..c5f0e63ffe8 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -229,7 +229,11 @@ + + + +