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