using System.ComponentModel; using System.Windows; using System.Windows.Controls; namespace WPFluent.Layout; #pragma warning disable CS8602 // Dereference of a possibly null reference. /// /// Defines a flexible grid area that consists of columns and rows. /// Depending on the orientation, either the rows or the columns are auto-generated, /// and the children's position is set according to their index. /// /// Partially based on work at http://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/ /// public class AutoGrid : Grid { public static readonly DependencyProperty ChildHorizontalAlignmentProperty = DependencyProperty.Register(nameof(ChildHorizontalAlignment), typeof(HorizontalAlignment?), typeof(AutoGrid), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildHorizontalAlignmentChanged)); public static readonly DependencyProperty ChildMarginProperty = DependencyProperty.Register(nameof(ChildMargin), typeof(Thickness?), typeof(AutoGrid), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildMarginChanged)); public static readonly DependencyProperty ChildVerticalAlignmentProperty = DependencyProperty.Register(nameof(ChildVerticalAlignment), propertyType: typeof(VerticalAlignment?), ownerType: typeof(AutoGrid), typeMetadata: new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildVerticalAlignmentChanged)); public static readonly DependencyProperty ColumnCountProperty = DependencyProperty.RegisterAttached(nameof(ColumnCount), typeof(int), typeof(AutoGrid), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(ColumnCountChanged))); public static readonly DependencyProperty ColumnsProperty = DependencyProperty.RegisterAttached(nameof(Columns), typeof(string), typeof(AutoGrid), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(ColumnsChanged))); public static readonly DependencyProperty ColumnWidthProperty = DependencyProperty.RegisterAttached(nameof(ColumnWidth), typeof(GridLength), typeof(AutoGrid), new FrameworkPropertyMetadata(GridLength.Auto, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(FixedColumnWidthChanged))); public static readonly DependencyProperty IsAutoIndexingProperty = DependencyProperty.Register(nameof(IsAutoIndexing), typeof(bool), typeof(AutoGrid), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(AutoGrid), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty RowCountProperty = DependencyProperty.RegisterAttached(nameof(RowCount), typeof(int), typeof(AutoGrid), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(RowCountChanged))); public static readonly DependencyProperty RowHeightProperty = DependencyProperty.RegisterAttached(nameof(RowHeight), typeof(GridLength), typeof(AutoGrid), new FrameworkPropertyMetadata(GridLength.Auto, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(FixedRowHeightChanged))); public static readonly DependencyProperty RowsProperty = DependencyProperty.RegisterAttached(nameof(Rows), typeof(string), typeof(AutoGrid), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(RowsChanged))); /// /// Gets or sets the child horizontal alignment. /// /// The child horizontal alignment. [Category("Layout"), Description("Presets the horizontal alignment of all child controls")] public HorizontalAlignment? ChildHorizontalAlignment { get => (HorizontalAlignment?)GetValue(ChildHorizontalAlignmentProperty); set => SetValue(ChildHorizontalAlignmentProperty, value); } /// /// Gets or sets the child margin. /// /// The child margin. [Category("Layout"), Description("Presets the margin of all child controls")] public Thickness? ChildMargin { get => (Thickness?)GetValue(ChildMarginProperty); set => SetValue(ChildMarginProperty, value); } /// /// Gets or sets the child vertical alignment. /// /// The child vertical alignment. [Category("Layout"), Description("Presets the vertical alignment of all child controls")] public VerticalAlignment? ChildVerticalAlignment { get => (VerticalAlignment?)GetValue(ChildVerticalAlignmentProperty); set => SetValue(ChildVerticalAlignmentProperty, value); } /// /// Gets or sets the column count /// [Category("Layout"), Description("Defines a set number of columns")] public int ColumnCount { get => (int)GetValue(ColumnCountProperty); set => SetValue(ColumnCountProperty, value); } /// /// Gets or sets the columns /// [Category("Layout"), Description("Defines all columns using comma separated grid length notation")] public string Columns { get => (string)GetValue(ColumnsProperty); set => SetValue(ColumnsProperty, value); } /// /// Gets or sets the fixed column width /// [Category("Layout"), Description("Presets the width of all columns set using the ColumnCount property")] public GridLength ColumnWidth { get => (GridLength)GetValue(ColumnWidthProperty); set => SetValue(ColumnWidthProperty, value); } /// /// Gets or sets a value indicating whether the children are automatically indexed. /// /// The default is true. /// Note that if children are already indexed, setting this property to false will not remove their indices. /// /// [Category("Layout"), Description("Set to false to disable the auto layout functionality")] public bool IsAutoIndexing { get => (bool)GetValue(IsAutoIndexingProperty); set => SetValue(IsAutoIndexingProperty, value); } /// /// Gets or sets the orientation. /// The default is Vertical. /// /// The orientation. [Category("Layout"), Description("Defines the directionality of the autolayout. Use vertical for a column first layout, horizontal for a row first layout.")] public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } /// /// Gets or sets the number of rows /// [Category("Layout"), Description("Defines a set number of rows")] public int RowCount { get => (int)GetValue(RowCountProperty); set => SetValue(RowCountProperty, value); } /// /// Gets or sets the fixed row height /// [Category("Layout"), Description("Presets the height of all rows set using the RowCount property")] public GridLength RowHeight { get => (GridLength)GetValue(RowHeightProperty); set => SetValue(RowHeightProperty, value); } /// /// Gets or sets the rows /// [Category("Layout"), Description("Defines all rows using comma separated grid length notation")] public string Rows { get => (string)GetValue(RowsProperty); set => SetValue(RowsProperty, value); } /// /// Handles the column count changed event /// public static void ColumnCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if ((int)e.NewValue < 0) return; var grid = d as AutoGrid; // look for an existing column definition for the height var width = GridLength.Auto; if (grid.ColumnDefinitions.Count > 0) width = grid.ColumnDefinitions[0].Width; // clear and rebuild grid.ColumnDefinitions.Clear(); for (int i = 0; i < (int)e.NewValue; i++) grid.ColumnDefinitions.Add( new ColumnDefinition() { Width = width }); } /// /// Handle the columns changed event /// public static void ColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (string.IsNullOrEmpty((string)e.NewValue)) return; var grid = d as AutoGrid; grid.ColumnDefinitions.Clear(); var defs = Parse((string)e.NewValue); foreach (var def in defs) grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = def }); } /// /// Handle the fixed column width changed event /// public static void FixedColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var grid = d as AutoGrid; // add a default column if missing if (grid.ColumnDefinitions.Count == 0) grid.ColumnDefinitions.Add(new ColumnDefinition()); // set all existing columns to this width for (int i = 0; i < grid.ColumnDefinitions.Count; i++) grid.ColumnDefinitions[i].Width = (GridLength)e.NewValue; } /// /// Handle the fixed row height changed event /// public static void FixedRowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var grid = d as AutoGrid; // add a default row if missing if (grid.RowDefinitions.Count == 0) grid.RowDefinitions.Add(new RowDefinition()); // set all existing rows to this height for (int i = 0; i < grid.RowDefinitions.Count; i++) grid.RowDefinitions[i].Height = (GridLength)e.NewValue; } /// /// Parse an array of grid lengths from comma delim text /// public static GridLength[] Parse(string text) { var tokens = text.Split(','); var definitions = new GridLength[tokens.Length]; for (var i = 0; i < tokens.Length; i++) { var str = tokens[i]; double value; // ratio if (str.Contains("*")) { if (!double.TryParse(str.Replace("*", string.Empty), out value)) value = 1.0; definitions[i] = new GridLength(value, GridUnitType.Star); continue; } // pixels if (double.TryParse(str, out value)) { definitions[i] = new GridLength(value); continue; } // auto definitions[i] = GridLength.Auto; } return definitions; } /// /// Handles the row count changed event /// public static void RowCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if ((int)e.NewValue < 0) return; var grid = d as AutoGrid; // look for an existing row to get the height var height = GridLength.Auto; if (grid.RowDefinitions.Count > 0) height = grid.RowDefinitions[0].Height; // clear and rebuild grid.RowDefinitions.Clear(); for (int i = 0; i < (int)e.NewValue; i++) grid.RowDefinitions.Add( new RowDefinition() { Height = height }); } /// /// Handle the rows changed event /// public static void RowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (string.IsNullOrEmpty((string)e.NewValue)) return; var grid = d as AutoGrid; grid.RowDefinitions.Clear(); var defs = Parse((string)e.NewValue); foreach (var def in defs) grid.RowDefinitions.Add(new RowDefinition() { Height = def }); } /// /// Measures the children of a in anticipation of arranging them during the pass. /// /// Indicates an upper limit size that should not be exceeded. /// /// that represents the required size to arrange child content. /// protected override Size MeasureOverride(Size constraint) { this.PerformLayout(); return base.MeasureOverride(constraint); } /// /// Called when [child horizontal alignment changed]. /// private static void OnChildHorizontalAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var grid = d as AutoGrid; foreach (UIElement child in grid.Children) { if (grid.ChildHorizontalAlignment.HasValue) child.SetValue(FrameworkElement.HorizontalAlignmentProperty, grid.ChildHorizontalAlignment); else child.SetValue(FrameworkElement.HorizontalAlignmentProperty, DependencyProperty.UnsetValue); } } /// /// Called when [child layout changed]. /// private static void OnChildMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var grid = d as AutoGrid; foreach (UIElement child in grid.Children) { if (grid.ChildMargin.HasValue) child.SetValue(FrameworkElement.MarginProperty, grid.ChildMargin); else child.SetValue(FrameworkElement.MarginProperty, DependencyProperty.UnsetValue); } } /// /// Called when [child vertical alignment changed]. /// private static void OnChildVerticalAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var grid = d as AutoGrid; foreach (UIElement child in grid.Children) { if (grid.ChildVerticalAlignment.HasValue) child.SetValue(FrameworkElement.VerticalAlignmentProperty, grid.ChildVerticalAlignment); else child.SetValue(FrameworkElement.VerticalAlignmentProperty, DependencyProperty.UnsetValue); } } ///// ///// Handled the redraw properties changed event ///// //private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) //{ } /// /// Apply child margins and layout effects such as alignment /// private void ApplyChildLayout(UIElement child) { if (ChildMargin != null) { child.SetIfDefault(FrameworkElement.MarginProperty, ChildMargin.Value); } if (ChildHorizontalAlignment != null) { child.SetIfDefault(FrameworkElement.HorizontalAlignmentProperty, ChildHorizontalAlignment.Value); } if (ChildVerticalAlignment != null) { child.SetIfDefault(FrameworkElement.VerticalAlignmentProperty, ChildVerticalAlignment.Value); } } /// /// Clamp a value to its maximum. /// private int Clamp(int value, int max) { return (value > max) ? max : value; } /// /// Perform the grid layout of row and column indexes /// private void PerformLayout() { var fillRowFirst = Orientation == Orientation.Horizontal; var rowCount = RowDefinitions.Count; var colCount = ColumnDefinitions.Count; if (rowCount == 0 || colCount == 0) return; var position = 0; var skip = new bool[rowCount, colCount]; foreach (UIElement child in Children) { var childIsCollapsed = child.Visibility == Visibility.Collapsed; if (IsAutoIndexing && !childIsCollapsed) { if (fillRowFirst) { var row = Clamp(position / colCount, rowCount - 1); var col = Clamp(position % colCount, colCount - 1); if (skip[row, col]) { position++; row = (position / colCount); col = (position % colCount); } Grid.SetRow(child, row); Grid.SetColumn(child, col); position += Grid.GetColumnSpan(child); var offset = Grid.GetRowSpan(child) - 1; while (offset > 0) { skip[row + offset--, col] = true; } } else { var row = Clamp(position % rowCount, rowCount - 1); var col = Clamp(position / rowCount, colCount - 1); if (skip[row, col]) { position++; row = position % rowCount; col = position / rowCount; } Grid.SetRow(child, row); Grid.SetColumn(child, col); position += Grid.GetRowSpan(child); var offset = Grid.GetColumnSpan(child) - 1; while (offset > 0) { skip[row, col + offset--] = true; } } } ApplyChildLayout(child); } } } file static class DependencyExtensions { /// /// Sets the value of the only if it hasn't been explicitly set. /// public static bool SetIfDefault(this DependencyObject o, DependencyProperty property, T value) { if (DependencyPropertyHelper.GetValueSource(o, property).BaseValueSource == BaseValueSource.Default) { o.SetValue(property, value); return true; } return false; } }