Browse Source

Merge branch 'master' into patch-1

Steven Kirk 7 years ago
parent
commit
70a8ebedb7

+ 1 - 1
src/Avalonia.Controls/ColumnDefinition.cs

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

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

+ 56 - 17
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;
             }

+ 14 - 9
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 - 0
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 - 0
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;
+        }
+    }
+}