Browse Source

Merge branch 'master' into patch-1

pull/2090/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
70a8ebedb7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Controls/ColumnDefinition.cs
  2. 154
      src/Avalonia.Controls/Grid.cs
  3. 73
      src/Avalonia.Controls/GridSplitter.cs
  4. 23
      src/Avalonia.Controls/Utils/GridLayout.cs
  5. 651
      src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs
  6. 284
      tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs

2
src/Avalonia.Controls/ColumnDefinition.cs

@ -88,4 +88,4 @@ namespace Avalonia.Controls
set { SetValue(WidthProperty, value); }
}
}
}
}

154
src/Avalonia.Controls/Grid.cs

@ -3,10 +3,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls
@ -44,6 +47,24 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> RowSpanProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>("RowSpan", 1);
public static readonly AttachedProperty<bool> IsSharedSizeScopeProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, bool>("IsSharedSizeScope", false);
protected override void OnMeasureInvalidated()
{
base.OnMeasureInvalidated();
_sharedSizeHost?.InvalidateMeasure(this);
}
private SharedSizeScopeHost _sharedSizeHost;
/// <summary>
/// Defines the SharedSizeScopeHost private property.
/// The ampersands are used to make accessing the property via xaml inconvenient.
/// </summary>
internal static readonly AttachedProperty<SharedSizeScopeHost> s_sharedSizeScopeHostProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, SharedSizeScopeHost>("&&SharedSizeScopeHost");
private ColumnDefinitions _columnDefinitions;
private RowDefinitions _rowDefinitions;
@ -51,6 +72,13 @@ namespace Avalonia.Controls
static Grid()
{
AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty);
IsSharedSizeScopeProperty.Changed.AddClassHandler<Control>(IsSharedSizeScopeChanged);
}
public Grid()
{
this.AttachedToVisualTree += Grid_AttachedToVisualTree;
this.DetachedFromVisualTree += Grid_DetachedFromVisualTree;
}
/// <summary>
@ -77,6 +105,7 @@ namespace Avalonia.Controls
_columnDefinitions = value;
_columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure());
_columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure();
}
}
@ -104,6 +133,7 @@ namespace Avalonia.Controls
_rowDefinitions = value;
_rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure());
_rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure();
}
}
@ -271,6 +301,11 @@ namespace Avalonia.Controls
_rowLayoutCache = rowLayout;
_columnLayoutCache = columnLayout;
if (_sharedSizeHost?.ParticipatesInScope(this) ?? false)
{
_sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult);
}
return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once.
@ -319,9 +354,21 @@ namespace Avalonia.Controls
var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = _columnLayoutCache;
var rowLayout = _rowLayoutCache;
var rowCache = _rowMeasureCache;
var columnCache = _columnMeasureCache;
if (_sharedSizeHost?.ParticipatesInScope(this) ?? false)
{
(rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache);
rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList);
columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList);
}
// Calculate for arrange result.
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
var columnResult = columnLayout.Arrange(finalSize.Width, columnCache);
var rowResult = rowLayout.Arrange(finalSize.Height, rowCache);
// Arrange the children.
foreach (var child in Children.OfType<Control>())
{
@ -350,6 +397,73 @@ namespace Avalonia.Controls
return finalSize;
}
/// <summary>
/// Tests whether this grid belongs to a shared size scope.
/// </summary>
/// <returns>True if the grid is registered in a shared size scope.</returns>
internal bool HasSharedSizeScope()
{
return _sharedSizeHost != null;
}
/// <summary>
/// Called when the SharedSizeScope for a given grid has changed.
/// Unregisters the grid from it's current scope and finds a new one (if any)
/// </summary>
/// <remarks>
/// This method, while not efficient, correctly handles nested scopes, with any order of scope changes.
/// </remarks>
internal void SharedScopeChanged()
{
_sharedSizeHost?.UnegisterGrid(this);
_sharedSizeHost = null;
var scope = this.GetVisualAncestors().OfType<Control>()
.FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty));
if (scope != null)
{
_sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty);
_sharedSizeHost.RegisterGrid(this);
}
InvalidateMeasure();
}
/// <summary>
/// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid
/// in it.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
var scope =
new Control[] { this }.Concat(this.GetVisualAncestors().OfType<Control>())
.FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty));
if (_sharedSizeHost != null)
throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!");
if (scope != null)
{
_sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty);
_sharedSizeHost.RegisterGrid(this);
}
}
/// <summary>
/// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_sharedSizeHost?.UnegisterGrid(this);
_sharedSizeHost = null;
}
/// <summary>
/// Get the safe column/columnspan and safe row/rowspan.
/// This method ensures that none of the children has a column/row outside the bounds of the definitions.
@ -426,5 +540,41 @@ namespace Avalonia.Controls
return value;
}
/// <summary>
/// Called when the value of <see cref="Grid.IsSharedSizeScopeProperty"/> changes for a control.
/// </summary>
/// <param name="source">The control that triggered the change.</param>
/// <param name="arg2">Change arguments.</param>
private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2)
{
var shouldDispose = (arg2.OldValue is bool d) && d;
if (shouldDispose)
{
var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost;
if (host == null)
throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!");
host.Dispose();
source.ClearValue(s_sharedSizeScopeHostProperty);
}
var shouldAssign = (arg2.NewValue is bool a) && a;
if (shouldAssign)
{
if (source.GetValue(s_sharedSizeScopeHostProperty) != null)
throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!");
source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost());
}
// if the scope has changed, notify the descendant grids that they need to update.
if (source.GetVisualRoot() != null && shouldAssign || shouldDispose)
{
var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType<Grid>();
foreach (var grid in participatingGrids)
grid.SharedScopeChanged();
}
}
}
}

73
src/Avalonia.Controls/GridSplitter.cs

@ -44,26 +44,52 @@ namespace Avalonia.Controls
protected override void OnDragDelta(VectorEventArgs e)
{
// WPF doesn't change anything when spliter is in the last row/column
// but resizes the splitter row/column when it's the first one.
// this is different, but more internally consistent.
if (_prevDefinition == null || _nextDefinition == null)
return;
var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y;
double max;
double min;
GetDeltaConstraints(out min, out max);
delta = Math.Min(Math.Max(delta, min), max);
foreach (var definition in _definitions)
var prevIsStar = IsStar(_prevDefinition);
var nextIsStar = IsStar(_nextDefinition);
if (prevIsStar && nextIsStar)
{
if (definition == _prevDefinition)
{
SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else if (definition == _nextDefinition)
{
SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (IsStar(definition))
foreach (var definition in _definitions)
{
SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
if (definition == _prevDefinition)
{
SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else if (definition == _nextDefinition)
{
SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (IsStar(definition))
{
SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
}
}
}
else if (prevIsStar)
{
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (nextIsStar)
{
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else
{
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
}
private double GetActualLength(DefinitionBase definition)
@ -71,7 +97,7 @@ namespace Avalonia.Controls
if (definition == null)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.ActualWidth ?? ((RowDefinition) definition).ActualHeight;
return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
}
private double GetMinLength(DefinitionBase definition)
@ -79,7 +105,7 @@ namespace Avalonia.Controls
if (definition == null)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MinWidth ?? ((RowDefinition) definition).MinHeight;
return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
}
private double GetMaxLength(DefinitionBase definition)
@ -87,13 +113,13 @@ namespace Avalonia.Controls
if (definition == null)
return 0;
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MaxWidth ?? ((RowDefinition) definition).MaxHeight;
return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
}
private bool IsStar(DefinitionBase definition)
{
var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.Width.IsStar ?? ((RowDefinition) definition).Height.IsStar;
return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
}
private void SetLengthInStars(DefinitionBase definition, double value)
@ -105,7 +131,20 @@ namespace Avalonia.Controls
}
else
{
((RowDefinition) definition).Height = new GridLength(value, GridUnitType.Star);
((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star);
}
}
private void SetLength(DefinitionBase definition, double value)
{
var columnDefinition = definition as ColumnDefinition;
if (columnDefinition != null)
{
columnDefinition.Width = new GridLength(value);
}
else
{
((RowDefinition)definition).Height = new GridLength(value);
}
}
@ -160,7 +199,7 @@ namespace Avalonia.Controls
}
if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column
.Where(c => Grid.GetColumn(c) == col)
.Any(c => c.GetType() != typeof (GridSplitter)))
.Any(c => c.GetType() != typeof(GridSplitter)))
{
return Orientation.Horizontal;
}

23
src/Avalonia.Controls/Utils/GridLayout.cs

@ -143,14 +143,17 @@ namespace Avalonia.Controls.Utils
/// <param name="containerLength">
/// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method.
/// </param>
/// <param name="conventions">
/// Overriding conventions that allows the algorithm to handle external inputa
/// </param>
/// <returns>
/// The measured result that containing the desired size and all the column/row lengths.
/// </returns>
[NotNull, Pure]
internal MeasureResult Measure(double containerLength)
internal MeasureResult Measure(double containerLength, IReadOnlyList<LengthConvention> conventions = null)
{
// Prepare all the variables that this method needs to use.
var conventions = _conventions.Select(x => x.Clone()).ToList();
conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList();
var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
var aggregatedLength = 0.0;
double starUnitLength;
@ -248,7 +251,7 @@ namespace Avalonia.Controls.Utils
// | min | max | | | min | | min max | max |
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
var desiredStarMin = AggregateAdditionalConventionsForStars(conventions);
var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions);
aggregatedLength += desiredStarMin;
// M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength.
@ -282,7 +285,7 @@ namespace Avalonia.Controls.Utils
// Returns the measuring result.
return new MeasureResult(containerLength, desiredLength, greedyDesiredLength,
conventions, dynamicConvention);
conventions, dynamicConvention, minLengths);
}
/// <summary>
@ -306,14 +309,14 @@ namespace Avalonia.Controls.Utils
if (finalLength - measure.ContainerLength > LayoutTolerance)
{
// If the final length is larger, we will rerun the whole measure.
measure = Measure(finalLength);
measure = Measure(finalLength, measure.LeanLengthList);
}
else if (finalLength - measure.ContainerLength < -LayoutTolerance)
{
// If the final length is smaller, we measure the M6/6 procedure only.
var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength);
measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength,
measure.LeanLengthList, dynamicConvention);
measure.LeanLengthList, dynamicConvention, measure.MinLengths);
}
return new ArrangeResult(measure.LengthList);
@ -370,7 +373,7 @@ namespace Avalonia.Controls.Utils
/// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param>
/// <returns>The total desired length of all the * length.</returns>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
private double AggregateAdditionalConventionsForStars(
private (List<double>, double) AggregateAdditionalConventionsForStars(
IReadOnlyList<LengthConvention> conventions)
{
// 1. Determine all one-span column's desired widths or row's desired heights.
@ -403,7 +406,7 @@ namespace Avalonia.Controls.Utils
lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0);
}
return lengthList.Sum() - fixedLength;
return (lengthList, lengthList.Sum() - fixedLength);
}
/// <summary>
@ -638,13 +641,14 @@ namespace Avalonia.Controls.Utils
/// Initialize a new instance of <see cref="MeasureResult"/>.
/// </summary>
internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength,
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions)
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions, IReadOnlyList<double> minLengths)
{
ContainerLength = containerLength;
DesiredLength = desiredLength;
GreedyDesiredLength = greedyDesiredLength;
LeanLengthList = leanConventions;
LengthList = expandedConventions;
MinLengths = minLengths;
}
/// <summary>
@ -674,6 +678,7 @@ namespace Avalonia.Controls.Utils
/// Gets the length list for each column/row.
/// </summary>
public IReadOnlyList<double> LengthList { get; }
public IReadOnlyList<double> MinLengths { get; }
}
/// <summary>

651
src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs

@ -0,0 +1,651 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Shared size scope implementation.
/// Shares the size information between participating grids.
/// An instance of this class is attached to every <see cref="Control"/> that has its
/// IsSharedSizeScope property set to true.
/// </summary>
internal sealed class SharedSizeScopeHost : IDisposable
{
private enum MeasurementState
{
Invalidated,
Measuring,
Cached
}
/// <summary>
/// Class containing the measured rows/columns for a single grid.
/// Monitors changes to the row/column collections as well as the SharedSizeGroup changes
/// for the individual items in those collections.
/// Notifies the <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
/// </summary>
private sealed class MeasurementCache : IDisposable
{
readonly CompositeDisposable _subscriptions;
readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>();
public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged;
public MeasurementCache(Grid grid)
{
Grid = grid;
Results = grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(grid.ColumnDefinitions)
.Select(d => new MeasurementResult(grid, d))
.ToList();
grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged;
grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged;
_subscriptions = new CompositeDisposable(
Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged),
grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged));
}
// method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid
private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
// route to collection changed as a Reset.
DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged)
{
if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup))
{
var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1));
var oldName = result.SizeGroup?.Name;
var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup;
_groupChanged.OnNext((oldName, newName, result));
}
}
private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
int offset = 0;
if (sender is ColumnDefinitions)
offset = Grid.RowDefinitions.Count;
var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>();
var oldItems = e.OldStartingIndex >= 0
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count)
: new List<MeasurementResult>();
void NotifyNewItems()
{
foreach (var item in newItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item));
}
}
void NotifyOldItems()
{
foreach (var item in oldItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item));
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Remove:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
NotifyOldItems();
break;
case NotifyCollectionChangedAction.Move:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, oldItems);
break;
case NotifyCollectionChangedAction.Replace:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyOldItems();
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Reset:
oldItems = Results;
newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(Grid.ColumnDefinitions)
.Select(d => new MeasurementResult(Grid, d))
.ToList();
NotifyOldItems();
NotifyNewItems();
break;
}
}
/// <summary>
/// Updates the Results collection with Grid Measure results.
/// </summary>
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
MeasurementState = MeasurementState.Cached;
for (int i = 0; i < Grid.RowDefinitions.Count; i++)
{
Results[i].MeasuredResult = rowResult.LengthList[i];
Results[i].MinLength = rowResult.MinLengths[i];
}
for (int i = 0; i < Grid.ColumnDefinitions.Count; i++)
{
Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i];
Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i];
}
}
/// <summary>
/// Clears the measurement cache, in preparation for the Measure pass.
/// </summary>
public void InvalidateMeasure()
{
var newItems = new List<MeasurementResult>();
var oldItems = new List<MeasurementResult>();
MeasurementState = MeasurementState.Invalidated;
Results.ForEach(r =>
{
r.MeasuredResult = double.NaN;
r.SizeGroup?.Reset();
});
}
/// <summary>
/// Clears the <see cref="IObservable{T}"/> subscriptions.
/// </summary>
public void Dispose()
{
_subscriptions.Dispose();
_groupChanged.OnCompleted();
}
/// <summary>
/// Gets the <see cref="Grid"/> for which this cache has been created.
/// </summary>
public Grid Grid { get; }
/// <summary>
/// Gets the <see cref="MeasurementState"/> of this cache.
/// </summary>
public MeasurementState MeasurementState { get; private set; }
/// <summary>
/// Gets the list of <see cref="MeasurementResult"/> instances.
/// </summary>
/// <remarks>
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
/// </remarks>
public List<MeasurementResult> Results { get; private set; }
}
/// <summary>
/// Class containing the Measure result for a single Row/Column in a grid.
/// </summary>
private class MeasurementResult
{
public MeasurementResult(Grid owningGrid, DefinitionBase definition)
{
OwningGrid = owningGrid;
Definition = definition;
MeasuredResult = double.NaN;
}
/// <summary>
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
/// </summary>
public DefinitionBase Definition { get; }
/// <summary>
/// Gets or sets the actual result of the Measure operation for this column.
/// </summary>
public double MeasuredResult { get; set; }
/// <summary>
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
/// </summary>
public double MinLength { get; set; }
/// <summary>
/// Gets or sets the <see cref="Group"/> that this result belongs to.
/// </summary>
public Group SizeGroup { get; set; }
/// <summary>
/// Gets the Grid that is the parent of the Row/Column
/// </summary>
public Grid OwningGrid { get; }
/// <summary>
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
/// </summary>
/// <returns>A tuple of length and the priority in the shared size group.</returns>
public (double length, int priority) GetPriorityLength()
{
var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height;
if (length.IsAbsolute)
return (MeasuredResult, 1);
if (length.IsAuto)
return (MeasuredResult, 2);
if (MinLength > 0)
return (MinLength, 3);
return (MeasuredResult, 4);
}
}
/// <summary>
/// Visitor class used to gather the final length for a given SharedSizeGroup.
/// </summary>
/// <remarks>
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
/// </remarks>
private class LentgthGatherer
{
/// <summary>
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
/// </summary>
public double Length { get; private set; }
private int gatheredPriority = 6;
/// <summary>
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
public void Visit(MeasurementResult result)
{
var (length, priority) = result.GetPriorityLength();
if (gatheredPriority < priority)
return;
gatheredPriority = priority;
if (gatheredPriority == priority)
{
Length = Math.Max(length,Length);
}
else
{
Length = length;
}
}
}
/// <summary>
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
/// </summary>
private class Group
{
private double? cachedResult;
private List<MeasurementResult> _results = new List<MeasurementResult>();
/// <summary>
/// Gets the name of the SharedSizeGroup.
/// </summary>
public string Name { get; }
public Group(string name)
{
Name = name;
}
/// <summary>
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
/// </summary>
public IReadOnlyList<MeasurementResult> Results => _results;
/// <summary>
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
/// </summary>
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value;
/// <summary>
/// Clears the previously cached result in preparation for measurement.
/// </summary>
public void Reset()
{
cachedResult = null;
}
/// <summary>
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
/// to this instance.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
public void Add(MeasurementResult result)
{
if (_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result");
result.SizeGroup = this;
_results.Add(result);
}
/// <summary>
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
public void Remove(MeasurementResult result)
{
if (!_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result");
result.SizeGroup = null;
_results.Remove(result);
}
private double Gather()
{
var visitor = new LentgthGatherer();
_results.ForEach(visitor.Visit);
return visitor.Length;
}
}
private readonly AvaloniaList<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>();
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>();
private bool _invalidating;
/// <summary>
/// Removes the SharedSizeScope and notifies all affected grids of the change.
/// </summary>
public void Dispose()
{
while (_measurementCaches.Any())
_measurementCaches[0].Grid.SharedScopeChanged();
}
/// <summary>
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
/// </summary>
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
internal void RegisterGrid(Grid toAdd)
{
if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd)))
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!");
var cache = new MeasurementCache(toAdd);
_measurementCaches.Add(cache);
AddGridToScopes(cache);
}
/// <summary>
/// Removes the registration for a grid in this SharedSizeScope.
/// </summary>
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
internal void UnegisterGrid(Grid toRemove)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!");
_measurementCaches.Remove(cache);
RemoveGridFromScopes(cache);
cache.Dispose();
}
/// <summary>
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
/// </summary>
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
/// <returns>True if the grid should forward its calls.</returns>
internal bool ParticipatesInScope(Grid toCheck)
{
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))
?.Results.Any(r => r.SizeGroup != null) ?? false;
}
/// <summary>
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
/// Forwards the same call to all affected grids in this scope.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
internal void InvalidateMeasure(Grid grid)
{
// prevent stack overflow
if (_invalidating)
return;
_invalidating = true;
InvalidateMeasureImpl(grid);
_invalidating = false;
}
/// <summary>
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!");
cache.UpdateMeasureResult(rowResult, columnResult);
}
/// <summary>
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
return (
Arrange(grid.RowDefinitions, rowResult),
Arrange(grid.ColumnDefinitions, columnResult)
);
}
/// <summary>
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
private void InvalidateMeasureImpl(Grid grid)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache");
// already invalidated the cache, early out.
if (cache.MeasurementState == MeasurementState.Invalidated)
return;
// we won't calculate, so we should not invalidate.
if (!ParticipatesInScope(grid))
return;
cache.InvalidateMeasure();
// maybe there is a condition to only call arrange on some of the calls?
grid.InvalidateMeasure();
// find all the scopes within the invalidated grid
var scopeNames = cache.Results
.Where(mr => mr.SizeGroup != null)
.Select(mr => mr.SizeGroup.Name)
.Distinct();
// find all grids related to those scopes
var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results)
.Select(r => r.OwningGrid)
.Where(g => g.IsMeasureValid)
.Distinct();
// invalidate them as well
foreach (var otherGrid in otherGrids)
{
InvalidateMeasureImpl(otherGrid);
}
}
/// <summary>
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
/// SharedSizeGroup
/// </summary>
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change)
{
RemoveFromGroup(change.oldName, change.result);
AddToGroup(change.newName, change.result);
}
/// <summary>
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
/// </summary>
/// <param name="definitions">Rows/Columns that were measured</param>
/// <param name="measureResult">The initial measurement result.</param>
/// <returns>Modified measure result</returns>
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult)
{
var conventions = measureResult.LeanLengthList.ToList();
var lengths = measureResult.LengthList.ToList();
var desiredLength = 0.0;
for (int i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
// for empty SharedSizeGroups pass on unmodified result.
if (string.IsNullOrEmpty(definition.SharedSizeGroup))
{
desiredLength += measureResult.LengthList[i];
continue;
}
var group = _groups[definition.SharedSizeGroup];
// Length calculated over all Definitions participating in a SharedSizeGroup.
var length = group.CalculatedLength;
conventions[i] = new GridLayout.LengthConvention(
new GridLength(length),
measureResult.LeanLengthList[i].MinLength,
measureResult.LeanLengthList[i].MaxLength
);
lengths[i] = length;
desiredLength += length;
}
return new GridLayout.MeasureResult(
measureResult.ContainerLength,
desiredLength,
measureResult.GreedyDesiredLength,//??
conventions,
lengths,
measureResult.MinLengths);
}
/// <summary>
/// Adds all measurement results for a grid to their repsective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
private void AddGridToScopes(MeasurementCache cache)
{
cache.GroupChanged.Subscribe(SharedGroupChanged);
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
AddToGroup(scopeName, result);
}
}
/// <summary>
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
private void AddToGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
_groups.Add(scopeName, group = new Group(scopeName));
group.Add(result);
}
/// <summary>
/// Removes all measurement results for a grid from their respective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
private void RemoveGridFromScopes(MeasurementCache cache)
{
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
RemoveFromGroup(scopeName, result);
}
}
/// <summary>
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
private void RemoveFromGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope");
group.Remove(result);
if (!group.Results.Any())
_groups.Remove(scopeName);
}
}
}

284
tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs

@ -0,0 +1,284 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class SharedSizeScopeTests
{
public SharedSizeScopeTests()
{
}
[Fact]
public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
}
[Fact]
public void All_Descendant_Grids_Are_Registered_When_Setting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.Child = scope;
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
}
[Fact]
public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
root.SetValue(Grid.IsSharedSizeScopeProperty, false);
Assert.All(grids, g => Assert.False(g.HasSharedSizeScope()));
Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty));
}
[Fact]
public void Size_Is_Propagated_Between_Grids()
{
var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))};
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Size_Propagation_Is_Constrained_To_Innermost_Scope()
{
var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) };
var innerScope = new Panel();
innerScope.Children.AddRange(grids);
innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true);
var outerGrid = CreateGrid(("A", new GridLength(0)));
var outerScope = new Panel();
outerScope.Children.AddRange(new[] { outerGrid, innerScope });
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = outerScope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Size_Is_Propagated_Between_Rows_And_Columns()
{
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,30"),
RowDefinitions = new RowDefinitions("*,10")
};
grid.ColumnDefinitions[1].SharedSizeGroup = "A";
grid.RowDefinitions[1].SharedSizeGroup = "A";
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = grid;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(30, grid.RowDefinitions[1].ActualHeight);
}
[Fact]
public void Size_Group_Changes_Are_Tracked()
{
var grids = new[] {
CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())),
CreateGrid(("A", new GridLength(30)), (null, new GridLength())) };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
grids[0].ColumnDefinitions[0].SharedSizeGroup = "A";
root.Measure(new Size(51, 51));
root.Arrange(new Rect(new Point(), new Point(51, 51)));
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth);
grids[0].ColumnDefinitions[0].SharedSizeGroup = null;
root.Measure(new Size(52, 52));
root.Arrange(new Rect(new Point(), new Point(52, 52)));
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Collection_Changes_Are_Tracked()
{
var grid = CreateGrid(
("A", new GridLength(20)),
("A", new GridLength(30)),
("A", new GridLength(40)),
(null, new GridLength()));
var scope = new Panel();
scope.Children.Add(grid);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth));
grid.ColumnDefinitions.RemoveAt(2);
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth));
grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" });
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth));
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" };
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth));
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" };
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth));
}
[Fact]
public void Size_Priorities_Are_Maintained()
{
var sizers = new List<Control>();
var grid = CreateGrid(
("A", new GridLength(20)),
("A", new GridLength(20, GridUnitType.Auto)),
("A", new GridLength(1, GridUnitType.Star)),
("A", new GridLength(1, GridUnitType.Star)),
(null, new GridLength()));
for (int i = 0; i < 3; i++)
sizers.Add(AddSizer(grid, i, 6 + i * 6));
var scope = new Panel();
scope.Children.Add(grid);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
grid.Measure(new Size(100, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// all in group are equal to the first fixed column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth));
grid.ColumnDefinitions[0].SharedSizeGroup = null;
grid.Measure(new Size(100, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// all in group are equal to width (MinWidth) of the sizer in the second column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth));
grid.ColumnDefinitions[1].SharedSizeGroup = null;
grid.Measure(new Size(double.PositiveInfinity, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// with no constraint star columns default to the MinWidth of the sizer in the column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth));
}
// grid creators
private Grid CreateGrid(params string[] columnGroups)
{
return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width)[] columns)
{
return CreateGrid(columns.Select(c =>
(c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns)
{
return CreateGrid(columns.Select(c =>
(c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns)
{
var columnDefinitions = new ColumnDefinitions();
columnDefinitions.AddRange(
columns.Select(c => new ColumnDefinition
{
SharedSizeGroup = c.name,
Width = c.width,
MinWidth = c.minWidth,
MaxWidth = c.maxWidth
})
);
var grid = new Grid
{
ColumnDefinitions = columnDefinitions
};
return grid;
}
private Control AddSizer(Grid grid, int column, double size = 30)
{
var ctrl = new Control { MinWidth = size, MinHeight = size };
ctrl.SetValue(Grid.ColumnProperty,column);
grid.Children.Add(ctrl);
return ctrl;
}
}
}
Loading…
Cancel
Save