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..df0f042571c --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsCode.bind @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..734f32f41a7 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GridExtensions/GridExtensionsPage.xaml @@ -0,0 +1,9 @@ + + + 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..945957c8dc1 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..d4cfa32bb48 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Grid/GridLayoutDefinition.cs @@ -0,0 +1,156 @@ +// 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 + { + private IDictionary _cellProperties; + + /// + /// 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. + /// + /// + /// Aread 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 => _cellProperties = ParseAreaDefinition(value); + } + + internal void Apply(Grid grid) + { + ApplyRowColumnDefinitions(grid); + UpdateChildren(grid); + } + + private static IDictionary ParseAreaDefinition(string str) + { + var def = new Dictionary(); + var table = str + .Split(";", StringSplitOptions.RemoveEmptyEntries) + .Select(row => row.Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(cell => cell.Trim()).ToList()) + .ToList(); + for (var i = 0; i < table.Count; ++i) + { + var tableRow = table[i]; + for (var j = 0; j < tableRow.Count; ++j) + { + var childName = tableRow[j]; + if (def.TryGetValue(childName, out var properties)) + { + var (row, _, column, _) = properties; + def[childName] = (Row: row, RowSpan: i - row + 1, Column: column, ColumnSpan: j - column + 1); + } + else + { + def[childName] = (Row: i, RowSpan: 1, Column: j, ColumnSpan: 1); + } + } + } + + return def; + } + + 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); + } + } + } + + } +}