diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
index 2018763e733..fd3cd192773 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
+++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
@@ -512,6 +512,9 @@
FocusBehaviorPage.xaml
+
+ GridExtensionsPage.xaml
+
MetadataControlPage.xaml
@@ -647,6 +650,7 @@
+
@@ -991,6 +995,10 @@
Designer
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsCode.bind
new file mode 100644
index 00000000000..a1dc0ab74e4
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsCode.bind
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resize to see the layout to be
+ switched dynamically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number Title Description
+
+
+
+
+
+
+
+
+
+
+ Number Title;
+
+ Description Description
+
+
+
+
+ 1
+
+
+ Lorem Ipsum
+
+
+ Lorem ipsum dolor sit amet...
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml
new file mode 100644
index 00000000000..4e03bb6b91a
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resize to see the layout to be
+ switched dynamically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number Title Description
+
+
+
+
+
+
+
+
+
+
+ Number Title;
+
+ Description Description
+
+
+
+
+ 1
+
+
+ Lorem Ipsum
+
+
+ Lorem ipsum dolor sit amet...
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml.cs
new file mode 100644
index 00000000000..f1d74667ce7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml.cs
@@ -0,0 +1,16 @@
+// 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 Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ public sealed partial class GridExtensionsPage : Page
+ {
+ public GridExtensionsPage()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
index 8cdfcc3a619..b378e3be33b 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
@@ -1289,6 +1289,14 @@
"XamlCodeFile": "EnumValuesExtensionXaml.bind",
"CodeFile": "EnumValuesExtensionCode.bind",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/extensions/EnumValuesExtension.md"
+ },
+ {
+ "Name": "GridExtensions",
+ "Type": "GridExtensionsPage",
+ "About": "Extensions to enable switching grid layouts dynamically",
+ "CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.UI/Extensions/GridExtensions",
+ "XamlCodeFile": "GridExtensionsCode.bind",
+ "Icon": "/Assets/Helpers.png"
}
]
},
@@ -1302,7 +1310,7 @@
"About": "Demonstrate the properties and events of the Gaze Interaction library",
"XamlCodeFile": "GazeInteractionXaml.bind",
"CodeFile": "GazeInteractionCode.bind",
- "CodeUrl" : "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.Input.GazeInteraction",
+ "CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.Input.GazeInteraction",
"Icon": "/SamplePages/GazeInteraction/GazeInteraction.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/gaze/GazeInteractionLibrary.md",
"ApiCheck": "Windows.Devices.Input.Preview.GazeInputSourcePreview"
@@ -1311,13 +1319,12 @@
"Name": "GazeTracing",
"Type": "GazeTracingPage",
"About": "Shows how to use the Windows 10 API for eye trackers",
- "CodeUrl" : "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.Input.GazeInteraction",
+ "CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.Input.GazeInteraction",
"XamlCodeFile": "GazeTracingXaml.bind",
"CodeFile": "GazeTracingCode.bind",
"Icon": "/SamplePages/GazeTracing/GazeTracing.png",
"ApiCheck": "Windows.Devices.Input.Preview.GazeInputSourcePreview",
- "DocumentationUrl" : "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/gaze/GazeInteractionLibrary.md"
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/gaze/GazeInteractionLibrary.md"
}
]
- }
-]
+ }]
diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.ActiveLayout.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.ActiveLayout.cs
new file mode 100644
index 00000000000..edd9c39ace9
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.ActiveLayout.cs
@@ -0,0 +1,43 @@
+// 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 Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI
+{
+ ///
+ /// Provides ActiveLayout attached property for element.
+ ///
+ public static partial class GridExtensions
+ {
+ ///
+ /// Attached for binding to a
+ ///
+ public static readonly DependencyProperty ActiveLayoutProperty =
+ DependencyProperty.RegisterAttached("ActiveLayout", typeof(string), typeof(GridExtensions), new PropertyMetadata(null, OnActiveLayoutChanged));
+
+ ///
+ /// Gets the associated with the specified
+ ///
+ /// The from which to get the associated value
+ /// The value associated with the or null
+ public static string GetActiveLayout(Grid obj) => (string)obj.GetValue(ActiveLayoutProperty);
+
+ ///
+ /// Sets the associated with the specified
+ ///
+ /// The to associated the to
+ /// The to bind to the
+ public static void SetActiveLayout(Grid obj, string value) => obj.SetValue(ActiveLayoutProperty, value);
+
+ private static void OnActiveLayoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is Grid grid)
+ {
+ UpdateLayout(grid);
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Layouts.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Layouts.cs
new file mode 100644
index 00000000000..8670b99380f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Layouts.cs
@@ -0,0 +1,48 @@
+// 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.Collections.Generic;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI
+{
+ using LayoutDictionary = System.Collections.Generic.IDictionary;
+
+ ///
+ /// Provides Layouts attached property for element.
+ ///
+ public static partial class GridExtensions
+ {
+ ///
+ /// Attached for binding to a
+ ///
+ public static readonly DependencyProperty LayoutsProperty =
+ DependencyProperty.RegisterAttached("Layouts", typeof(LayoutDictionary), typeof(GridExtensions), new PropertyMetadata(null));
+
+ ///
+ /// Gets the associated with the specified
+ ///
+ /// The from which to get the associated value
+ /// The value associated with the or null
+ public static LayoutDictionary GetLayouts(Grid obj)
+ {
+ var dictionary = (LayoutDictionary)obj.GetValue(LayoutsProperty);
+ if (dictionary is null)
+ {
+ dictionary = new Dictionary();
+ SetLayouts(obj, dictionary);
+ }
+
+ return dictionary;
+ }
+
+ ///
+ /// Sets the associated with the specified
+ ///
+ /// The to associated the to
+ /// The to bind to the
+ public static void SetLayouts(Grid obj, LayoutDictionary value) => obj.SetValue(LayoutsProperty, value);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Private.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Private.cs
new file mode 100644
index 00000000000..d6054496456
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridExtensions.Private.cs
@@ -0,0 +1,41 @@
+// 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 Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI
+{
+ ///
+ /// Private methods on GridExtensions to enable dynamic layout switching capability.
+ ///
+ public partial class GridExtensions
+ {
+ private static readonly DependencyProperty LoadedCallbackRegisteredProperty =
+ DependencyProperty.RegisterAttached("LoadedCallbackRegistered", typeof(bool), typeof(GridExtensions), new PropertyMetadata(false));
+
+ private static void UpdateLayout(Grid grid)
+ {
+ if (grid.IsLoaded)
+ {
+ if (GetLayouts(grid).TryGetValue(GetActiveLayout(grid), out var layout))
+ {
+ layout.Apply(grid);
+ }
+ }
+ else if (!(bool)grid.GetValue(LoadedCallbackRegisteredProperty))
+ {
+ grid.SetValue(LoadedCallbackRegisteredProperty, true);
+ grid.Loaded += OnGridLoaded;
+ }
+ }
+
+ private static void OnGridLoaded(object sender, RoutedEventArgs args)
+ {
+ var grid = (Grid)sender;
+ grid.Loaded -= OnGridLoaded;
+ UpdateLayout(grid);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridLayoutDefinition.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridLayoutDefinition.cs
new file mode 100644
index 00000000000..b2e4bfda6d9
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridLayoutDefinition.cs
@@ -0,0 +1,186 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Markup;
+
+namespace Microsoft.Toolkit.Uwp.UI
+{
+ ///
+ /// The data structure to define a possible grid layout.
+ ///
+ [ContentProperty(Name = "AreaDefinition")]
+ public class GridLayoutDefinition
+ {
+ ///
+ /// Dictionary to store the parsed result of area definition.
+ ///
+ ///
+ /// The string key is x:Name attribute
+ /// of child elements of the grid, while the tuple value is the row index, row span value, column index and column span value of that
+ /// specific element.
+ ///
+ private IDictionary _cellProperties = new Dictionary();
+
+ ///
+ /// Gets a list of ColumnDefinition objects defined for this layout.
+ ///
+ public List ColumnDefinitions { get; } = new List();
+
+ ///
+ /// Gets a list of RowDefinition objects defined for this layout.
+ ///
+ public List RowDefinitions { get; } = new List();
+
+ ///
+ /// Sets area definition for this layout.
+ ///
+ ///
+ /// Area definition string should list the of Grid elements in row-major order.
+ /// Elements in same row should be separated with whitespaces, and rows should be separated with semicolons.
+ /// Row and column spans can be expressed by repeating element names.
+ ///
+ ///
+ /// A simple 2x3 grid layout:
+ /// .-----------.
+ /// | A | B | C |
+ /// |---|---|---|
+ /// | D | E | F |
+ /// '-----------'
+ /// A B C;
+ /// D E F;
+ ///
+ /// A 3x3 grid layout where the first and third rows span 3 columns:
+ /// .-----------------------.
+ /// | header |
+ /// |------.--------.-------|
+ /// | left | center | right |
+ /// |------'--------'-------|
+ /// | footer |
+ /// '-----------------------'
+ /// header header header;
+ /// left center right;
+ /// footer footer footer;
+ ///
+ /// A 3x3 grid layout with row span and column span.
+ /// .----------------------.
+ /// | | header |
+ /// | nav |--------.-------|
+ /// | | center | right |
+ /// |-----'--------'-------|
+ /// | footer |
+ /// '----------------------'
+ /// nav header header;
+ /// nav center right;
+ /// footer footer footer;
+ ///
+ /// Incorrect usage:
+ /// A B C;
+ /// D A E;
+ /// will result in
+ /// .-----------.
+ /// | : B | C |
+ /// |...A...|---|
+ /// | D : | E |
+ /// '-----------'
+ /// Dotted lines are used to illustrate A, B and D children are overlapped.
+ /// This usage is not really invalid (it does not throw),
+ /// but this API is not expected to be used like this.
+ ///
+ public string AreaDefinition
+ {
+ set => ParseAreaDefinition(value);
+ }
+
+ internal void Apply(Grid grid)
+ {
+ ApplyRowColumnDefinitions(grid);
+ UpdateChildren(grid);
+ }
+
+ ///
+ /// Parse the area definition string, and save it in .
+ ///
+ /// The area definition string.
+ ///
+ /// The parsing goes as:
+ /// 1. Split the string by semicolon, as the rows separator is semicolon.
+ /// 2. Split every row string by space.
+ /// The (i,j)th value of resulting 2 dimensional array is
+ /// x:Name attribute
+ /// of the element that should be placed at (i,j)th cell in the grid.
+ /// If name of an element repeats, that element should span over all the cells with its name.
+ ///
+ private void ParseAreaDefinition(string str)
+ {
+ _cellProperties.Clear();
+
+ // split the area definition by semicolon and then split every row by space.
+ var table = str
+ .Split(";", StringSplitOptions.RemoveEmptyEntries)
+ .Select(row => row.Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(cell => cell.Trim()));
+
+ // Since the table is Enumerable of Enumerable rather than list of list, we need to iterate over it via foreach loop.
+ // foreach loop doesn't have a current index, but we need current index in the loop body, thus the explicit i and j declaration.
+ var i = 0;
+ foreach (var tableRow in table)
+ {
+ var j = 0;
+ foreach (var childName in tableRow)
+ {
+ if (_cellProperties.TryGetValue(childName, out var properties))
+ {
+ // if childName already exists in _cellProperties, it means the element name is repeating in the area definition.
+ // we should keep row index and column index, while setting row span and column span.
+ // The number of cells to span over is (currentIndex - startingIndex + 1).
+ var (row, _, column, _) = properties;
+ _cellProperties[childName] = (Row: row, RowSpan: i - row + 1, Column: column, ColumnSpan: j - column + 1);
+ }
+ else
+ {
+ // otherwise, the element should be placed at (i, j)th cell and span over 1 row, 1 column.
+ _cellProperties[childName] = (Row: i, RowSpan: 1, Column: j, ColumnSpan: 1);
+ }
+
+ ++j;
+ }
+
+ ++i;
+ }
+ }
+
+ private void ApplyRowColumnDefinitions(Grid grid)
+ {
+ grid.ColumnDefinitions.Clear();
+ foreach (var def in ColumnDefinitions)
+ {
+ grid.ColumnDefinitions.Add(def);
+ }
+
+ grid.RowDefinitions.Clear();
+ foreach (var def in RowDefinitions)
+ {
+ grid.RowDefinitions.Add(def);
+ }
+ }
+
+ private void UpdateChildren(Grid grid)
+ {
+ foreach (var child in grid.Children)
+ {
+ if (child is FrameworkElement element && _cellProperties.TryGetValue(element.Name, out var property))
+ {
+ child.SetValue(Grid.ColumnProperty, property.Column);
+ child.SetValue(Grid.RowProperty, property.Row);
+ child.SetValue(Grid.ColumnSpanProperty, property.ColumnSpan);
+ child.SetValue(Grid.RowSpanProperty, property.RowSpan);
+ }
+ }
+ }
+ }
+}