diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.cs
index 0b14ec3b282..ddd4a9f5c6d 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.cs
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.cs
@@ -88,39 +88,102 @@ private static void ScalePropertyChanged(DependencyObject d, DependencyPropertyC
}
}
+ private bool IsPositiveRealNumber(double value) => !double.IsNaN(value) && !double.IsInfinity(value) && value > 0;
+
+ private Size _lastMeasuredSize;
+
///
protected override Size MeasureOverride(Size availableSize)
{
- return base.MeasureOverride(CalculateConstrainedSize(availableSize));
+ CalculateConstrainedSize(ref availableSize);
+
+ _lastMeasuredSize = availableSize;
+
+ // Call base.MeasureOverride so any child elements know what room there is to work with.
+ // Don't return this though. An image that hasn't loaded yet for example will request very little space.
+ base.MeasureOverride(_lastMeasuredSize);
+ return _lastMeasuredSize;
}
+ //// Our Arrange pass should just use the value we calculated in Measure, so we don't have extra work to do (at least the ContentPresenter we use presently does it for us.)
+
///
protected override Size ArrangeOverride(Size finalSize)
{
- return base.ArrangeOverride(CalculateConstrainedSize(finalSize));
+ // Even though we requested in measure to be a specific size, that doesn't mean our parent
+ // panel respected that request. Grid for instance can by default Stretch and if you don't
+ // set Horizontal/VerticalAlignment on the control it won't constrain as we expect.
+ // We could also be in a StackPanel/ScrollViewer where it wants to provide as much space as possible.
+ // However, if we always re-calculate even if we are provided the proper finalSize, this can trigger
+ // multiple arrange passes and cause a rounding error in layout. Therefore, we only want to
+ // re-calculate if we think we will have a significant impact.
+ //// TODO: Not sure what good tolerance is here
+ if (Math.Abs(finalSize.Width - _lastMeasuredSize.Width) > 1.5 ||
+ Math.Abs(finalSize.Height - _lastMeasuredSize.Height) > 1.5)
+ {
+ CalculateConstrainedSize(ref finalSize);
+
+ // Copy again so if Arrange is re-triggered we won't re-calculate.
+ _lastMeasuredSize = finalSize;
+ }
+
+ return base.ArrangeOverride(finalSize);
}
- private Size CalculateConstrainedSize(Size initialSize)
+ private void CalculateConstrainedSize(ref Size availableSize)
{
- var availableSize = new Size(initialSize.Width * ScaleX, initialSize.Height * ScaleY);
+ var hasWidth = IsPositiveRealNumber(availableSize.Width);
+ var hasHeight = IsPositiveRealNumber(availableSize.Height);
+
+ if (!hasWidth && !hasHeight)
+ {
+ // We have infinite space, like a ScrollViewer with both scrolling directions
+ // Ask child how big they want to be first.
+ availableSize = base.MeasureOverride(availableSize);
+
+ hasWidth = IsPositiveRealNumber(availableSize.Width);
+ hasHeight = IsPositiveRealNumber(availableSize.Height);
+
+ if (!hasWidth && !hasHeight)
+ {
+ // At this point we have no way to determine a constraint, the Panel won't do anything
+ // This should be rare? We don't really have a way to provide a warning here.
+ return;
+ }
+ }
+
+ // Scale size first before we constrain aspect ratio
+ availableSize.Width *= ScaleX;
+ availableSize.Height *= ScaleY;
// If we don't have an Aspect Ratio, just return the scaled value.
if (ReadLocalValue(AspectRatioProperty) == DependencyProperty.UnsetValue)
{
- return availableSize;
+ return;
}
// Calculate the Aspect Ratio constraint based on the newly scaled size.
var currentAspect = availableSize.Width / availableSize.Height;
- var desiredAspect = AspectRatio.Value;
- if (currentAspect >= desiredAspect)
+ if (!hasWidth)
+ {
+ // If available width is infinite, set width based on height
+ availableSize.Width = availableSize.Height * AspectRatio;
+ }
+ else if (!hasHeight)
+ {
+ // If avalable height is infinite, set height based on width
+ availableSize.Height = availableSize.Width / AspectRatio;
+ }
+ else if (currentAspect > AspectRatio)
{
- return new Size(availableSize.Height * desiredAspect, availableSize.Height);
+ // If the container aspect ratio is wider than our aspect ratio, set width based on height
+ availableSize.Width = availableSize.Height * AspectRatio;
}
else
{
- return new Size(availableSize.Width, availableSize.Width / desiredAspect);
+ // If the container aspect ratio is taller than our aspect ratio, set height based on width
+ availableSize.Height = availableSize.Width / AspectRatio;
}
}
}
diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.cs
new file mode 100644
index 00000000000..a9ca91c1735
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.cs
@@ -0,0 +1,108 @@
+// 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
+{
+ [TestClass]
+ public class Test_ConstrainedBox : VisualUITestBase
+ {
+ [TestCategory("ConstrainedBox")]
+ [TestMethod]
+ public async Task Test_ConstrainedBox_Normal_Horizontal()
+ {
+ 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(child.ActualWidth, 200, "Width unexpected");
+ Assert.AreEqual(child.ActualHeight, 100, "Height unexpected");
+ });
+ }
+
+ [TestCategory("ConstrainedBox")]
+ [TestMethod]
+ public async Task Test_ConstrainedBox_Normal_ScaleX()
+ {
+ 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;
+
+ 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(child.ActualWidth, 100);
+ Assert.AreEqual(child.ActualHeight, 200);
+
+ // 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(position.X, 50);
+ Assert.AreEqual(position.Y, 0);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.UWP/UnitTestApp.xaml b/UnitTests/UnitTests.UWP/UnitTestApp.xaml
index dee4f8c1209..87f176b9a0f 100644
--- a/UnitTests/UnitTests.UWP/UnitTestApp.xaml
+++ b/UnitTests/UnitTests.UWP/UnitTestApp.xaml
@@ -11,7 +11,7 @@
-
+
Cat
@@ -43,7 +43,9 @@
-
+
+
+
diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
index 0323d553a9a..d99679aac02 100644
--- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
+++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
@@ -1,4 +1,4 @@
-
+
Debug
@@ -231,6 +231,7 @@
+