Skip to content

Commit 5171b99

Browse files
Merge pull request #4169 from michael-hawker/mhawker/constrained-box-fixes
Add Value Coercion to ConstrainedBox
2 parents 2d74076 + 85a457a commit 5171b99

File tree

7 files changed

+599
-3
lines changed

7 files changed

+599
-3
lines changed

Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.Properties.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public int MultipleX
6161
/// Identifies the <see cref="MultipleX"/> property.
6262
/// </summary>
6363
public static readonly DependencyProperty MultipleXProperty =
64-
DependencyProperty.Register(nameof(MultipleX), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null));
64+
DependencyProperty.Register(nameof(MultipleX), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged));
6565

6666
/// <summary>
6767
/// 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
7676
/// Identifies the <see cref="MultipleY"/> property.
7777
/// </summary>
7878
public static readonly DependencyProperty MultipleYProperty =
79-
DependencyProperty.Register(nameof(MultipleY), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null));
79+
DependencyProperty.Register(nameof(MultipleY), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged));
8080

8181
/// <summary>
8282
/// Gets or sets aspect Ratio to use for the contents of the Panel (after scaling).
@@ -93,11 +93,71 @@ public AspectRatio AspectRatio
9393
public static readonly DependencyProperty AspectRatioProperty =
9494
DependencyProperty.Register(nameof(AspectRatio), typeof(AspectRatio), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged));
9595

96+
private bool _propertyUpdating;
97+
9698
private static void ConstraintPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
9799
{
98-
if (d is ConstrainedBox panel)
100+
if (d is ConstrainedBox panel && !panel._propertyUpdating)
99101
{
102+
panel._propertyUpdating = true;
103+
104+
panel.CoerceValues();
105+
100106
panel.InvalidateMeasure();
107+
108+
panel._propertyUpdating = false;
109+
}
110+
}
111+
112+
private void CoerceValues()
113+
{
114+
// Check if scale properties are in range
115+
if (!double.IsNaN(ScaleX))
116+
{
117+
if (ScaleX < 0)
118+
{
119+
ScaleX = 0;
120+
}
121+
else if (ScaleX > 1.0)
122+
{
123+
ScaleX = 1.0;
124+
}
125+
}
126+
else
127+
{
128+
ScaleX = 1.0;
129+
}
130+
131+
if (!double.IsNaN(ScaleY))
132+
{
133+
if (ScaleY < 0)
134+
{
135+
ScaleY = 0;
136+
}
137+
else if (ScaleY > 1.0)
138+
{
139+
ScaleY = 1.0;
140+
}
141+
}
142+
else
143+
{
144+
ScaleY = 1.0;
145+
}
146+
147+
// Clear invalid values less than 0 for other properties
148+
if (ReadLocalValue(MultipleXProperty) is int value && value <= 0)
149+
{
150+
ClearValue(MultipleXProperty);
151+
}
152+
153+
if (ReadLocalValue(MultipleYProperty) is int value2 && value2 <= 0)
154+
{
155+
ClearValue(MultipleYProperty);
156+
}
157+
158+
if (ReadLocalValue(AspectRatioProperty) is AspectRatio ratio && ratio <= 0)
159+
{
160+
ClearValue(AspectRatioProperty);
101161
}
102162
}
103163
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.Toolkit.Uwp;
8+
using Microsoft.Toolkit.Uwp.UI;
9+
using Microsoft.Toolkit.Uwp.UI.Controls;
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
12+
using Windows.Foundation;
13+
using Windows.UI.Xaml;
14+
using Windows.UI.Xaml.Controls;
15+
using Windows.UI.Xaml.Markup;
16+
17+
namespace UnitTests.UWP.UI.Controls
18+
{
19+
/// <summary>
20+
/// These tests check whether the inner alignment of the box within it's parent works as expected.
21+
/// </summary>
22+
public partial class Test_ConstrainedBox : VisualUITestBase
23+
{
24+
// For this test we're testing within the confines of a 200x200 box to position a contrained
25+
// 50x100 element in all the different alignment combinations.
26+
[TestCategory("ConstrainedBox")]
27+
[TestMethod]
28+
[DataRow("Left", 0, "Center", 50, DisplayName = "LeftCenter")]
29+
[DataRow("Left", 0, "Top", 0, DisplayName = "LeftTop")]
30+
[DataRow("Center", 75, "Top", 0, DisplayName = "CenterTop")]
31+
[DataRow("Right", 150, "Top", 0, DisplayName = "RightTop")]
32+
[DataRow("Right", 150, "Center", 50, DisplayName = "RightCenter")]
33+
[DataRow("Right", 150, "Bottom", 100, DisplayName = "RightBottom")]
34+
[DataRow("Center", 75, "Bottom", 100, DisplayName = "CenterBottom")]
35+
[DataRow("Left", 0, "Bottom", 100, DisplayName = "LeftBottom")]
36+
[DataRow("Center", 75, "Center", 50, DisplayName = "CenterCenter")]
37+
public async Task Test_ConstrainedBox_Alignment_Aspect(string horizontalAlignment, int expectedLeft, string verticalAlignment, int expectedTop)
38+
{
39+
await App.DispatcherQueue.EnqueueAsync(async () =>
40+
{
41+
var treeRoot = XamlReader.Load(@$"<Page
42+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
43+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
44+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
45+
<Grid x:Name=""ParentGrid""
46+
Width=""200"" Height=""200"">
47+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" AspectRatio=""1:2"" MaxHeight=""100""
48+
UseLayoutRounding=""False""
49+
HorizontalAlignment=""{horizontalAlignment}""
50+
VerticalAlignment=""{verticalAlignment}"">
51+
<Border HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"" Background=""Red""/>
52+
</controls:ConstrainedBox>
53+
</Grid>
54+
</Page>") as FrameworkElement;
55+
56+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
57+
58+
// Initialize Visual Tree
59+
await SetTestContentAsync(treeRoot);
60+
61+
var grid = treeRoot.FindChild("ParentGrid") as Grid;
62+
63+
Assert.IsNotNull(grid, "Could not find the ParentGrid in tree.");
64+
65+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
66+
67+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
68+
69+
// Force Layout calculations
70+
panel.UpdateLayout();
71+
72+
var child = panel.Content as Border;
73+
74+
Assert.IsNotNull(child, "Could not find inner Border");
75+
76+
// Check Size
77+
Assert.AreEqual(50, child.ActualWidth, 0.01, "Actual width does not meet expected value of 50");
78+
Assert.AreEqual(100, child.ActualHeight, 0.01, "Actual height does not meet expected value of 100");
79+
80+
// Check inner Positioning, we do this from the Grid as the ConstainedBox also modifies its own size
81+
// and is hugging the child.
82+
var position = grid.CoordinatesTo(child);
83+
84+
Assert.AreEqual(expectedLeft, position.X, 0.01, "X position does not meet expected value of 0");
85+
Assert.AreEqual(expectedTop, position.Y, 0.01, "Y position does not meet expected value of 50");
86+
});
87+
}
88+
}
89+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.Toolkit.Uwp;
8+
using Microsoft.Toolkit.Uwp.UI;
9+
using Microsoft.Toolkit.Uwp.UI.Controls;
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
12+
using Windows.Foundation;
13+
using Windows.UI.Xaml;
14+
using Windows.UI.Xaml.Controls;
15+
using Windows.UI.Xaml.Markup;
16+
17+
namespace UnitTests.UWP.UI.Controls
18+
{
19+
/// <summary>
20+
/// These tests check for the various values which can be coerced and changed if out of bounds for each property.
21+
/// </summary>
22+
public partial class Test_ConstrainedBox : VisualUITestBase
23+
{
24+
[TestCategory("ConstrainedBox")]
25+
[TestMethod]
26+
public async Task Test_ConstrainedBox_Coerce_Scale()
27+
{
28+
await App.DispatcherQueue.EnqueueAsync(async () =>
29+
{
30+
var treeRoot = XamlReader.Load(@"<Page
31+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
32+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
33+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
34+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" ScaleX=""0.5"" ScaleY=""0.5""/>
35+
</Page>") as FrameworkElement;
36+
37+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
38+
39+
// Initialize Visual Tree
40+
await SetTestContentAsync(treeRoot);
41+
42+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
43+
44+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
45+
46+
// Check Size
47+
Assert.AreEqual(0.5, panel.ScaleX, 0.01, "ScaleX does not meet expected initial value of 0.5");
48+
Assert.AreEqual(0.5, panel.ScaleY, 0.01, "ScaleY does not meet expected initial value of 0.5");
49+
50+
// Change values now to be invalid
51+
panel.ScaleX = double.NaN;
52+
panel.ScaleY = double.NaN;
53+
54+
Assert.AreEqual(1.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 1.0 after change.");
55+
Assert.AreEqual(1.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 1.0 after change.");
56+
57+
// Change values now to be invalid
58+
panel.ScaleX = double.NegativeInfinity;
59+
panel.ScaleY = double.NegativeInfinity;
60+
61+
Assert.AreEqual(0.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 0.0 after change.");
62+
Assert.AreEqual(0.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 0.0 after change.");
63+
64+
// Change values now to be invalid
65+
panel.ScaleX = double.PositiveInfinity;
66+
panel.ScaleY = double.PositiveInfinity;
67+
68+
Assert.AreEqual(1.0, panel.ScaleX, 0.01, "ScaleX does not meet expected value of 1.0 after change.");
69+
Assert.AreEqual(1.0, panel.ScaleY, 0.01, "ScaleY does not meet expected value of 1.0 after change.");
70+
});
71+
}
72+
73+
[TestCategory("ConstrainedBox")]
74+
[TestMethod]
75+
public async Task Test_ConstrainedBox_Coerce_Multiple()
76+
{
77+
await App.DispatcherQueue.EnqueueAsync(async () =>
78+
{
79+
var treeRoot = XamlReader.Load(@"<Page
80+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
81+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
82+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
83+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" MultipleX=""32"" MultipleY=""32""/>
84+
</Page>") as FrameworkElement;
85+
86+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
87+
88+
// Initialize Visual Tree
89+
await SetTestContentAsync(treeRoot);
90+
91+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
92+
93+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
94+
95+
// Check Size
96+
Assert.AreEqual(32, panel.MultipleX, "MultipleX does not meet expected value of 32");
97+
Assert.AreEqual(32, panel.MultipleY, "MultipleY does not meet expected value of 32");
98+
99+
// Change values now to be invalid
100+
panel.MultipleX = 0;
101+
panel.MultipleY = int.MinValue;
102+
103+
Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.MultipleXProperty), "MultipleX does not meet expected value of UnsetValue after change.");
104+
Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.MultipleYProperty), "MultipleY does not meet expected value of UnsetValue after change.");
105+
});
106+
}
107+
108+
[TestCategory("ConstrainedBox")]
109+
[TestMethod]
110+
public async Task Test_ConstrainedBox_Coerce_AspectRatio()
111+
{
112+
await App.DispatcherQueue.EnqueueAsync(async () =>
113+
{
114+
var treeRoot = XamlReader.Load(@"<Page
115+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
116+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
117+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
118+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" AspectRatio=""1.25:3.5""/>
119+
</Page>") as FrameworkElement;
120+
121+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
122+
123+
// Initialize Visual Tree
124+
await SetTestContentAsync(treeRoot);
125+
126+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
127+
128+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
129+
130+
// Check Size
131+
Assert.AreEqual(1.25 / 3.5, panel.AspectRatio, 0.01, "ApectRatio does not meet expected value of 1.25/3.5");
132+
133+
// Change values now to be invalid
134+
panel.AspectRatio = -1;
135+
136+
Assert.AreEqual(DependencyProperty.UnsetValue, panel.ReadLocalValue(ConstrainedBox.AspectRatioProperty), "AspectRatio does not meet expected value of UnsetValue after change.");
137+
});
138+
}
139+
}
140+
}

UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.Combined.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
namespace UnitTests.UWP.UI.Controls
1919
{
20+
/// <summary>
21+
/// These tests check multiple constraints are applied together in the correct order.
22+
/// </summary>
2023
public partial class Test_ConstrainedBox : VisualUITestBase
2124
{
2225
[TestCategory("ConstrainedBox")]

0 commit comments

Comments
 (0)