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); + } + } + } + } +}