diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs
index 23d45edf6a..b8e9f0ff42 100644
--- a/samples/Sandbox/MainWindow.axaml.cs
+++ b/samples/Sandbox/MainWindow.axaml.cs
@@ -6,17 +6,11 @@ using Avalonia.Win32.WinRT.Composition;
namespace Sandbox
{
- public class MainWindow : Window
+ public partial class MainWindow : Window
{
public MainWindow()
{
- this.InitializeComponent();
- this.AttachDevTools();
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
+ InitializeComponent();
}
}
}
diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj
index f23e391a2a..d2e66988e0 100644
--- a/samples/Sandbox/Sandbox.csproj
+++ b/samples/Sandbox/Sandbox.csproj
@@ -4,6 +4,7 @@
WinExe
net6.0
true
+ true
@@ -17,4 +18,5 @@
+
diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs
index d62acc0d52..dd99c40cd3 100644
--- a/src/Avalonia.Base/Animation/Animation.cs
+++ b/src/Avalonia.Base/Animation/Animation.cs
@@ -200,7 +200,7 @@ namespace Avalonia.Animation
///
/// The animation setter.
/// The property animator value.
- public static void SetAnimator(IAnimationSetter setter,
+ public static void SetAnimator(IAnimationSetter setter,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)]
Type value)
{
@@ -319,7 +319,7 @@ namespace Avalonia.Animation
if (animators.Count == 1)
{
var subscription = animators[0].Apply(this, control, clock, match, onComplete);
-
+
if (subscription is not null)
{
subscriptions.Add(subscription);
@@ -348,9 +348,11 @@ namespace Avalonia.Animation
if (onComplete != null)
{
- Task.WhenAll(completionTasks!).ContinueWith(
- (_, state) => ((Action)state!).Invoke(),
- onComplete);
+ Task.WhenAll(completionTasks!)
+ .ContinueWith((_, state) => ((Action)state!).Invoke()
+ , onComplete
+ , TaskScheduler.FromCurrentSynchronizationContext()
+ );
}
}
return new CompositeDisposable(subscriptions);
diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs
index 94955a18ae..f47738f2e4 100644
--- a/src/Avalonia.Base/Layout/LayoutManager.cs
+++ b/src/Avalonia.Base/Layout/LayoutManager.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Layout
///
public class LayoutManager : ILayoutManager, IDisposable
{
- private const int MaxPasses = 3;
+ private const int MaxPasses = 10;
private readonly Layoutable _owner;
private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid);
private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid);
@@ -249,10 +249,12 @@ namespace Avalonia.Layout
{
var control = _toMeasure.Dequeue();
- if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
+ if (!control.IsMeasureValid)
{
Measure(control);
}
+
+ _toArrange.Enqueue(control);
}
}
@@ -262,7 +264,7 @@ namespace Avalonia.Layout
{
var control = _toArrange.Dequeue();
- if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
+ if (!control.IsArrangeValid)
{
Arrange(control);
}
@@ -297,8 +299,6 @@ namespace Avalonia.Layout
{
control.Measure(control.PreviousMeasure.Value);
}
-
- _toArrange.Enqueue(control);
}
return true;
diff --git a/src/Avalonia.Base/Layout/LayoutQueue.cs b/src/Avalonia.Base/Layout/LayoutQueue.cs
index 24adeb0793..48efa501f2 100644
--- a/src/Avalonia.Base/Layout/LayoutQueue.cs
+++ b/src/Avalonia.Base/Layout/LayoutQueue.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using Avalonia.Logging;
namespace Avalonia.Layout
{
@@ -48,10 +49,21 @@ namespace Avalonia.Layout
{
_loopQueueInfo.TryGetValue(item, out var info);
- if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
+ if (!info.Active)
{
- _inner.Enqueue(item);
- _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+ if (info.Count < _maxEnqueueCountPerLoop)
+ {
+ _inner.Enqueue(item);
+ _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+ }
+ else
+ {
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log(
+ this,
+ "Layout cycle detected. Item {Item} was enqueued {Count} times.",
+ item,
+ info.Count);
+ }
}
}
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index ed88b73149..08f327d048 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/src/Avalonia.Base/Layout/Layoutable.cs
@@ -776,10 +776,24 @@ namespace Avalonia.Layout
// All changes to visibility cause the parent element to be notified.
this.GetVisualParent()?.ChildDesiredSizeChanged(this);
- // We only invalidate outselves when visibility is changed to true.
if (change.GetNewValue())
{
+ // We only invalidate ourselves when visibility is changed to true.
InvalidateMeasure();
+
+ // If any descendant had its measure/arrange invalidated while we were hidden,
+ // they will need to to be registered with the layout manager now that they
+ // are again effectively visible. If IsEffectivelyVisible becomes an observable
+ // property then we can piggy-pack on that; for the moment we do this manually.
+ if (VisualRoot is ILayoutRoot layoutRoot)
+ {
+ var count = VisualChildren.Count;
+
+ for (var i = 0; i < count; ++i)
+ {
+ (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager);
+ }
+ }
}
}
}
@@ -804,6 +818,30 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
+ private void AncestorBecameVisible(ILayoutManager layoutManager)
+ {
+ if (!IsVisible)
+ return;
+
+ if (!IsMeasureValid)
+ {
+ layoutManager.InvalidateMeasure(this);
+ InvalidateVisual();
+ }
+ else if (!IsArrangeValid)
+ {
+ layoutManager.InvalidateArrange(this);
+ InvalidateVisual();
+ }
+
+ var count = VisualChildren.Count;
+
+ for (var i = 0; i < count; ++i)
+ {
+ (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager);
+ }
+ }
+
///
/// Called when the layout manager raises a LayoutUpdated event.
///
diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs
new file mode 100644
index 0000000000..d523808d32
--- /dev/null
+++ b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Avalonia.Metadata;
+
+///
+/// Defines how compiler should split avalonia list string value before parsing individual items.
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+public sealed class AvaloniaListAttribute : Attribute
+{
+ ///
+ /// Separator used to split input string.
+ /// Default value is ','.
+ ///
+ public string[]? Separators { get; init; }
+
+ ///
+ /// Split options used to split input string.
+ /// Default value is RemoveEmptyEntries with TrimEntries.
+ ///
+ // StringSplitOptions.TrimEntries = 2, but only on net6 target.
+ public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2;
+}
diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs
index c850647bf4..63a54731e0 100644
--- a/src/Avalonia.Controls/DefinitionList.cs
+++ b/src/Avalonia.Controls/DefinitionList.cs
@@ -1,9 +1,11 @@
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Collections;
+using Avalonia.Metadata;
namespace Avalonia.Controls
{
+ [AvaloniaList(Separators = new [] { ",", " " })]
public abstract class DefinitionList : AvaloniaList where T : DefinitionBase
{
public DefinitionList()
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index 57ed67b508..b2c138599e 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
@@ -7,8 +7,8 @@ namespace Avalonia.Controls.Generators
///
///
/// When creating a container for an item from a , the following
- /// method order should be followed:
- ///
+ /// process should be followed:
+ ///
/// - should first be called if the item is
/// derived from the class. If this method returns true then the
/// item itself should be used as the container.
@@ -19,9 +19,29 @@ namespace Avalonia.Controls.Generators
/// - The container should then be added to the panel using
///
/// - Finally, should be called.
- /// - When the item is ready to be recycled, should
- /// be called if returned false.
///
+ /// NOTE: If in the first step above returns true
+ /// then the above steps should be carried out a single time; the first time the item is
+ /// displayed. Otherwise the steps should be carried out each time a new container is realized
+ /// for an item.
+ ///
+ /// When unrealizing a container, the following process should be followed:
+ ///
+ /// - If for the item returned true then the item
+ /// cannot be unrealized or recycled.
+ /// - Otherwise, should be called for the container
+ /// - If recycling is supported then the container should be added to a recycle pool.
+ /// - It is assumed that recyclable containers will not be removed from the panel but instead
+ /// hidden from view using e.g. `container.IsVisible = false`.
+ ///
+ /// When recycling an unrealized container, the following process should be followed:
+ ///
+ /// - An element should be taken from the recycle pool.
+ /// - The container should be made visible.
+ /// - method should be called for the
+ /// container.
+ /// - should be called.
+ ///
/// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only
/// concerns itself with generating and clearing item containers; it does not maintain a
/// record of the currently realized containers, that responsibility is delegated to the
@@ -65,7 +85,7 @@ namespace Avalonia.Controls.Generators
/// The index of the item to display.
///
/// If is true for an item, then this method
- /// only needs to be called a single time, otherwise this method should be called after the
+ /// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
///
@@ -80,10 +100,11 @@ namespace Avalonia.Controls.Generators
/// The item being displayed.
/// The index of the item being displayed.
///
- /// This method should be called when a container has been fully prepared and added
+ /// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
- /// It should be called regardless of the result of
- /// .
+ /// It must be called regardless of the result of
+ /// but if that method returned true then
+ /// must be called only a single time.
///
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@@ -102,6 +123,12 @@ namespace Avalonia.Controls.Generators
/// Undoes the effects of the method.
///
/// The container control.
+ ///
+ /// This method must be called when a container is unrealized. The container must have
+ /// already have been removed from the virtualizing panel's list of realized containers before
+ /// this method is called. This method must not be called if
+ /// returned true for the item.
+ ///
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
[Obsolete("Use ItemsControl.ContainerFromIndex")]
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 329a0fa6ab..736c338c10 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -1,5 +1,5 @@
using System;
-
+using Avalonia.Collections;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
@@ -442,7 +442,7 @@ namespace Avalonia.Controls.Presenters
var contentTemplate = ContentTemplate;
var oldChild = Child;
var newChild = CreateChild(content, oldChild, contentTemplate);
- var logicalChildren = Host?.LogicalChildren ?? LogicalChildren;
+ var logicalChildren = GetEffectiveLogicalChildren();
// Remove the old child if we're not recycling it.
if (newChild != oldChild)
@@ -488,6 +488,9 @@ namespace Avalonia.Controls.Presenters
}
+ private IAvaloniaList GetEffectiveLogicalChildren()
+ => Host?.LogicalChildren ?? LogicalChildren;
+
///
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
@@ -692,7 +695,7 @@ namespace Avalonia.Controls.Presenters
else if (Child != null)
{
VisualChildren.Remove(Child);
- LogicalChildren.Remove(Child);
+ GetEffectiveLogicalChildren().Remove(Child);
((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
_recyclingDataTemplate = null;
diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
index 796ee8433a..5a6f9fc4f9 100644
--- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
+++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
@@ -67,9 +67,12 @@ namespace Avalonia.Controls.Presenters
for (var i = 0; i < count; ++i)
{
var c = children[index + i];
+
if (!c.IsSet(ItemIsOwnContainerProperty))
+ {
itemsControl.RemoveLogicalChild(children[i + index]);
- generator.ClearItemContainer(c);
+ generator.ClearItemContainer(c);
+ }
}
children.RemoveRange(index, count);
diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
index 3926a23d43..ed419ad89b 100644
--- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
+++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
@@ -100,7 +100,7 @@ namespace Avalonia.Controls
}
}
- public PullDirection PullDirection
+ internal PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 7d474fabc6..85a35a3489 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -285,6 +285,11 @@ namespace Avalonia.Controls
///
public event EventHandler? Closed;
+ ///
+ /// Gets or sets a method called when the TopLevel's scaling changes.
+ ///
+ public event EventHandler? ScalingChanged;
+
///
/// Gets or sets the client size of the window.
///
@@ -428,7 +433,7 @@ namespace Avalonia.Controls
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
///
- double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
+ public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
@@ -590,6 +595,7 @@ namespace Avalonia.Controls
protected virtual void HandleScalingChanged(double scaling)
{
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
+ ScalingChanged?.Invoke(this, EventArgs.Empty);
}
private static bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received)
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index 806d7e320b..70ffd218b1 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -45,6 +45,7 @@ namespace Avalonia.Controls
private TreeView? _treeView;
private Control? _header;
+ private Control? _headerPresenter;
private int _level;
private bool _templateApplied;
private bool _deferredBringIntoViewFlag;
@@ -255,15 +256,16 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
- if (_header is InputElement previousInputMethod)
+ if (_headerPresenter is InputElement previousInputMethod)
{
previousInputMethod.DoubleTapped -= HeaderDoubleTapped;
}
_header = e.NameScope.Find("PART_Header");
+ _headerPresenter = e.NameScope.Find("PART_HeaderPresenter");
_templateApplied = true;
- if (_header is InputElement im)
+ if (_headerPresenter is InputElement im)
{
im.DoubleTapped += HeaderDoubleTapped;
}
diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
index 8dbfb2c957..11bbaa11c4 100644
--- a/src/Avalonia.Controls/Utils/RealizedStackElements.cs
+++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
@@ -353,7 +353,10 @@ namespace Avalonia.Controls.Utils
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
+ {
+ _elements[i] = null;
recycleElement(element);
+ }
}
_elements.RemoveRange(start, end - start);
@@ -389,10 +392,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
- foreach (var e in _elements)
+ for (var i = 0; i < _elements.Count; i++)
{
- if (e is not null)
+ if (_elements[i] is Control e)
+ {
+ _elements[i] = null;
recycleElement(e);
+ }
}
_startU = _firstIndex = 0;
@@ -422,7 +428,10 @@ namespace Avalonia.Controls.Utils
for (var i = 0; i < endIndex; ++i)
{
if (_elements[i] is Control e)
+ {
+ _elements[i] = null;
recycleElement(e, i + FirstIndex);
+ }
}
_elements.RemoveRange(0, endIndex);
@@ -453,7 +462,10 @@ namespace Avalonia.Controls.Utils
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
+ {
+ _elements[i] = null;
recycleElement(e, i + FirstIndex);
+ }
}
_elements.RemoveRange(startIndex, _elements.Count - startIndex);
@@ -470,13 +482,13 @@ namespace Avalonia.Controls.Utils
if (_elements is null || _elements.Count == 0)
return;
- var i = FirstIndex;
-
- foreach (var e in _elements)
+ for (var i = 0; i < _elements.Count; i++)
{
- if (e is not null)
- recycleElement(e, i);
- ++i;
+ if (_elements[i] is Control e)
+ {
+ _elements[i] = null;
+ recycleElement(e, i + FirstIndex);
+ }
}
_startU = _firstIndex = 0;
diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
index da0ff1eb69..28d6a83309 100644
--- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs
@@ -168,7 +168,13 @@ namespace Avalonia.Controls
protected internal override Control? ContainerFromIndex(int index)
{
- return index == _realizedIndex ? _realized : null;
+ if (index < 0 || index >= Items.Count)
+ return null;
+ if (index == _realizedIndex)
+ return _realized;
+ if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
+ return c;
+ return null;
}
protected internal override IEnumerable? GetRealizedContainers()
@@ -264,7 +270,6 @@ namespace Avalonia.Controls
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
- generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs
index a95d4f1ffa..ed92f30454 100644
--- a/src/Avalonia.Controls/VirtualizingPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingPanel.cs
@@ -76,6 +76,11 @@ namespace Avalonia.Controls
/// The container for the item at the specified index within the item collection, if the
/// item is realized; otherwise, null.
///
+ ///
+ /// Note for implementors: if the item at the the specified index is an ItemIsOwnContainer
+ /// item that has previously been realized, then the item should be returned even if it
+ /// currently falls outside the realized viewport.
+ ///
protected internal abstract Control? ContainerFromIndex(int index);
///
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index 77268c7831..e0768edfa4 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
-using System.Reflection;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
@@ -326,7 +325,17 @@ namespace Avalonia.Controls
return _realizedElements?.Elements.Where(x => x is not null)!;
}
- protected internal override Control? ContainerFromIndex(int index) => _realizedElements?.GetElement(index);
+ protected internal override Control? ContainerFromIndex(int index)
+ {
+ if (index < 0 || index >= Items.Count)
+ return null;
+ if (_realizedElements?.GetElement(index) is { } realized)
+ return realized;
+ if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
+ return c;
+ return null;
+ }
+
protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
protected internal override Control? ScrollIntoView(int index)
@@ -556,7 +565,6 @@ namespace Avalonia.Controls
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
- InvalidateHack(e);
return e;
}
@@ -578,7 +586,6 @@ namespace Avalonia.Controls
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
- generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
@@ -705,39 +712,6 @@ namespace Avalonia.Controls
}
}
- private static void InvalidateHack(Control c)
- {
- bool HasInvalidations(Control c)
- {
- if (!c.IsMeasureValid)
- return true;
-
- for (var i = 0; i < c.VisualChildren.Count; ++i)
- {
- if (c.VisualChildren[i] is Control child)
- {
- if (!child.IsMeasureValid || HasInvalidations(child))
- return true;
- }
- }
-
- return false;
- }
-
- void Invalidate(Control c)
- {
- c.InvalidateMeasure();
- for (var i = 0; i < c.VisualChildren.Count; ++i)
- {
- if (c.VisualChildren[i] is Control child)
- Invalidate(child);
- }
- }
-
- if (HasInvalidations(c))
- Invalidate(c);
- }
-
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
{
if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index c2523207e4..ac47e744e0 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -83,6 +83,19 @@ namespace Avalonia.Controls
///
/// Occurs when the window is resized.
///
+ ///
+ /// Although this event is similar to the event, they are
+ /// conceptually different:
+ ///
+ /// - is a window-level event, fired when a resize notification arrives
+ /// from the platform windowing subsystem. The event args contain details of the source of
+ /// the resize event in the property. This
+ /// event is raised before layout has been run on the window's content.
+ /// - is a layout-level event, fired when a layout pass
+ /// completes on a control. is present on all controls
+ /// and is fired when the control's size changes for any reason, including a
+ /// event in the case of a Window.
+ ///
public event EventHandler? Resized;
public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl;
@@ -116,6 +129,11 @@ namespace Avalonia.Controls
set { SetValue(TopmostProperty, value); }
}
+ ///
+ /// Gets the scaling factor for Window positioning and sizing.
+ ///
+ public double DesktopScaling => PlatformImpl?.DesktopScaling ?? 1;
+
///
/// Activates the window.
///
@@ -232,14 +250,16 @@ namespace Avalonia.Controls
{
FrameSize = PlatformImpl?.FrameSize;
- if (ClientSize != clientSize)
+ var clientSizeChanged = ClientSize != clientSize;
+
+ ClientSize = clientSize;
+ OnResized(new WindowResizedEventArgs(clientSize, reason));
+
+ if (clientSizeChanged)
{
- ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
Renderer.Resized(clientSize);
}
-
- OnResized(new WindowResizedEventArgs(clientSize, reason));
}
///
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
index 66be6b5041..b0aea64994 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
@@ -39,16 +39,6 @@ namespace Avalonia.Diagnostics.Views
AdornerLayer.SetIsClipEnabled(_adorner, false);
}
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
-
- ((TreePageViewModel)DataContext!).ClipboardCopyRequested += (sender, s) =>
- {
- TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(s);
- };
- }
-
protected void AddAdorner(object? sender, PointerEventArgs e)
{
var node = (TreeNode?)((Control)sender!).DataContext;
@@ -108,9 +98,27 @@ namespace Avalonia.Diagnostics.Views
_currentLayer = null;
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == DataContextProperty)
+ {
+ if (change.GetOldValue
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
index b5f1220bc8..eff3920b12 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
@@ -75,7 +75,6 @@
MinHeight="{TemplateBinding MinHeight}"
TemplatedControl.IsTemplateFocusTarget="True">
a.Type == types.AvaloniaListAttribute);
+ if (attribute is not null)
+ {
+ if (attribute.Properties.TryGetValue("Separators", out var separatorsArray))
+ {
+ separators = ((Array)separatorsArray)?.OfType().ToArray();
+ }
+
+ if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj))
+ {
+ splitOptions = (StringSplitOptions)splitOptionsObj;
+ }
+ }
+
items = text.Split(separators, splitOptions ^ trimOption);
// Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested.
if (splitOptions.HasFlag(trimOption))
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 8ab84f4615..b5c0c7734d 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType InheritDataTypeFromItemsAttribute { get; }
public IXamlType MarkupExtensionOptionAttribute { get; }
public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
+ public IXamlType AvaloniaListAttribute { get; }
public IXamlType AvaloniaList { get; }
public IXamlType OnExtensionType { get; }
public IXamlType UnsetValueType { get; }
@@ -143,6 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute");
MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
+ AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute");
AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1");
OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");
AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject,
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
index 5d1025f30d..e5254eb1b2 160000
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
@@ -1 +1 @@
-Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393
+Subproject commit e5254eb1b2017f78a92acd466c8fa1e47401056b
diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
index 9f13520086..45a6efdd4a 100644
--- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
@@ -59,6 +59,29 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.False(control.Arranged);
}
+ [Fact]
+ public void Lays_Out_Descendents_That_Were_Invalidated_While_Ancestor_Was_Not_Visible()
+ {
+ // Issue #11076
+ var control = new LayoutTestControl();
+ var parent = new Decorator { Child = control };
+ var grandparent = new Decorator { Child = parent };
+ var root = new LayoutTestRoot { Child = grandparent };
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ grandparent.IsVisible = false;
+ control.InvalidateMeasure();
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ grandparent.IsVisible = true;
+
+ root.LayoutManager.ExecuteLayoutPass();
+
+ Assert.True(control.IsMeasureValid);
+ Assert.True(control.IsArrangeValid);
+ }
+
[Fact]
public void Arranges_InvalidateArranged_Control()
{
@@ -491,5 +514,38 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.True(parent.IsMeasureValid);
Assert.True(parent.IsArrangeValid);
}
+
+ [Fact]
+ public void Grandparent_Can_Invalidate_Root_Measure_During_Arrange()
+ {
+ // Issue #11161.
+ var child = new LayoutTestControl();
+ var parent = new LayoutTestControl { Child = child };
+ var grandparent = new LayoutTestControl { Child = parent };
+ var root = new LayoutTestRoot { Child = grandparent };
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ grandparent.DoArrangeOverride = (_, s) =>
+ {
+ root.InvalidateMeasure();
+ return s;
+ };
+ grandparent.CallBaseArrange = true;
+
+ child.InvalidateMeasure();
+ grandparent.InvalidateMeasure();
+
+ root.LayoutManager.ExecuteLayoutPass();
+
+ Assert.True(child.IsMeasureValid);
+ Assert.True(child.IsArrangeValid);
+ Assert.True(parent.IsMeasureValid);
+ Assert.True(parent.IsArrangeValid);
+ Assert.True(grandparent.IsMeasureValid);
+ Assert.True(grandparent.IsArrangeValid);
+ Assert.True(root.IsMeasureValid);
+ Assert.True(root.IsArrangeValid);
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs
index 62de81006e..d85c7ed9bc 100644
--- a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs
+++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs
@@ -10,21 +10,41 @@ namespace Avalonia.Base.UnitTests.Layout
public bool Arranged { get; set; }
public Func DoMeasureOverride { get; set; }
public Func DoArrangeOverride { get; set; }
+ public bool CallBaseMeasure { get; set; }
+ public bool CallBaseArrange { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
Measured = true;
- return DoMeasureOverride != null ?
- DoMeasureOverride(this, availableSize) :
- base.MeasureOverride(availableSize);
+
+ if (DoMeasureOverride is not null)
+ {
+ var overrideResult = DoMeasureOverride(this, availableSize);
+ return CallBaseMeasure ?
+ base.MeasureOverride(overrideResult) :
+ overrideResult;
+ }
+ else
+ {
+ return base.MeasureOverride(availableSize);
+ }
}
protected override Size ArrangeOverride(Size finalSize)
{
Arranged = true;
- return DoArrangeOverride != null ?
- DoArrangeOverride(this, finalSize) :
- base.ArrangeOverride(finalSize);
+
+ if (DoArrangeOverride is not null)
+ {
+ var overrideResult = DoArrangeOverride(this, finalSize);
+ return CallBaseArrange ?
+ base.ArrangeOverride(overrideResult) :
+ overrideResult;
+ }
+ else
+ {
+ return base.ArrangeOverride(finalSize);
+ }
}
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
index 6624d13165..2a35787f3b 100644
--- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
@@ -261,6 +261,71 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Can_Move_Forward_Back_Forward()
+ {
+ using var app = Start();
+ var items = new[] { "foo", "bar" };
+ var target = new Carousel
+ {
+ Template = CarouselTemplate(),
+ ItemsSource = items,
+ };
+
+ Prepare(target);
+
+ target.SelectedIndex = 1;
+ Layout(target);
+
+ Assert.Equal(1, target.SelectedIndex);
+
+ target.SelectedIndex = 0;
+ Layout(target);
+
+ Assert.Equal(0, target.SelectedIndex);
+
+ target.SelectedIndex = 1;
+ Layout(target);
+
+ Assert.Equal(1, target.SelectedIndex);
+ }
+
+ [Fact]
+ public void Can_Move_Forward_Back_Forward_With_Control_Items()
+ {
+ // Issue #11119
+ using var app = Start();
+ var items = new[] { new Canvas(), new Canvas() };
+ var target = new Carousel
+ {
+ Template = CarouselTemplate(),
+ ItemsSource = items,
+ };
+
+ Prepare(target);
+
+ target.SelectedIndex = 1;
+ Layout(target);
+
+ Assert.Equal(1, target.SelectedIndex);
+
+ target.SelectedIndex = 0;
+ Layout(target);
+
+ Assert.Equal(0, target.SelectedIndex);
+
+ target.SelectedIndex = 1;
+ target.PropertyChanged += (s, e) =>
+ {
+ if (e.Property == Carousel.SelectedIndexProperty)
+ {
+ }
+ };
+ Layout(target);
+
+ Assert.Equal(1, target.SelectedIndex);
+ }
+
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
private static void Prepare(Carousel target)
diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
index bece711426..d5e4693666 100644
--- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
@@ -359,16 +359,21 @@ namespace Avalonia.Controls.UnitTests
target.Presenter.ApplyTemplate();
Assert.Equal(target, target.Presenter.Child.GetLogicalParent());
+ Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren);
root.Child = null;
Assert.Null(target.Template);
target.Content = null;
+
+ Assert.Empty(target.LogicalChildren);
+
root.Child = target;
target.Content = "Bar";
Assert.Equal(target, target.Presenter.Child.GetLogicalParent());
+ Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren);
}
private static FuncControlTemplate GetTemplate()
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 1a0ea5fdab..5e741cdc1d 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -828,6 +828,19 @@ namespace Avalonia.Controls.UnitTests
Layout(target);
}
+ [Fact]
+ public void ItemIsOwnContainer_Content_Should_Not_Be_Cleared_When_Removed()
+ {
+ // Issue #11128.
+ using var app = Start();
+ var item = new ContentPresenter { Content = "foo" };
+ var target = CreateTarget(items: new[] { item });
+
+ target.Items.RemoveAt(0);
+
+ Assert.Equal("foo", item.Content);
+ }
+
private static ItemsControl CreateTarget(
object? dataContext = null,
IBinding? displayMemberBinding = null,
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
index db6460e8aa..dfdcd09bf9 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
@@ -676,12 +676,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
[Fact]
public void Moving_Selected_Item_Should_Clear_Selection()
{
- var items = new AvaloniaList-
- {
- new Item(),
- new Item(),
- };
-
+ using var app = Start();
+ var items = new ObservableCollection { "foo", "bar" };
var target = new SelectingItemsControl
{
ItemsSource = items,
@@ -706,7 +702,46 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.NotNull(receivedArgs);
Assert.Empty(receivedArgs.AddedItems);
Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
- Assert.All(items, x => Assert.False(x.IsSelected));
+ }
+
+ [Fact]
+ public void Moving_Selected_Container_Should_Not_Clear_Selection()
+ {
+ var items = new AvaloniaList
-
+ {
+ new Item(),
+ new Item(),
+ };
+
+ var target = new SelectingItemsControl
+ {
+ ItemsSource = items,
+ Template = Template(),
+ };
+
+ Prepare(target);
+ target.SelectedIndex = 1;
+
+ Assert.Equal(items[1], target.SelectedItem);
+ Assert.Equal(1, target.SelectedIndex);
+
+ var receivedArgs = new List();
+
+ target.SelectionChanged += (_, args) => receivedArgs.Add(args);
+
+ var moved = items[1];
+ items.Move(1, 0);
+
+ // Because the moved container is still marked as selected on the insert part of the
+ // move, it will remain selected.
+ Assert.Same(moved, target.SelectedItem);
+ Assert.Equal(0, target.SelectedIndex);
+ Assert.NotNull(receivedArgs);
+ Assert.Equal(2, receivedArgs.Count);
+ Assert.Equal(new[] { moved }, receivedArgs[0].RemovedItems);
+ Assert.Equal(new[] { moved }, receivedArgs[1].AddedItems);
+ Assert.True(items[0].IsSelected);
+ Assert.False(items[1].IsSelected);
}
[Fact]
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
index 0817979e33..daebc1e709 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
@@ -1024,6 +1024,90 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { 15 }, SelectedContainers(target));
}
+ [Fact]
+ public void Can_Change_Selection_For_Containers_Outside_Of_Viewport()
+ {
+ // Issue #11119
+ using var app = Start();
+ var items = Enumerable.Range(0, 100).Select(x => new TestContainer
+ {
+ Content = $"Item {x}",
+ Height = 100,
+ }).ToList();
+
+ // Create a SelectingItemsControl with a virtualizing stack panel.
+ var target = CreateTarget(itemsSource: items, virtualizing: true);
+ target.AutoScrollToSelectedItem = false;
+
+ var panel = Assert.IsType(target.ItemsPanelRoot);
+ var scroll = panel.FindAncestorOfType()!;
+
+ // Select item 1.
+ target.SelectedIndex = 1;
+
+ // Scroll item 1 and 2 out of view.
+ scroll.Offset = new(0, 1000);
+ Layout(target);
+
+ Assert.Equal(10, panel.FirstRealizedIndex);
+ Assert.Equal(19, panel.LastRealizedIndex);
+
+ // Select item 2 now that items 1 and 2 are both unrealized.
+ target.SelectedIndex = 2;
+
+ // The selection should be updated.
+ Assert.Empty(SelectedContainers(target));
+ Assert.Equal(2, target.SelectedIndex);
+ Assert.Same(items[2], target.SelectedItem);
+ Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes);
+ Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems);
+
+ // Scroll selected item back into view.
+ scroll.Offset = new(0, 0);
+ Layout(target);
+
+ // The selection should be preserved.
+ Assert.Equal(new[] { 2 }, SelectedContainers(target));
+ Assert.Equal(2, target.SelectedIndex);
+ Assert.Same(items[2], target.SelectedItem);
+ Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes);
+ Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems);
+ }
+
+ [Fact]
+ public void Selection_Is_Not_Cleared_On_Recycling_Containers()
+ {
+ using var app = Start();
+ var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList();
+
+ // Create a SelectingItemsControl that creates containers that raise IsSelectedChanged,
+ // with a virtualizing stack panel.
+ var target = CreateTarget(
+ itemsSource: items,
+ virtualizing: true);
+ target.AutoScrollToSelectedItem = false;
+
+ var panel = Assert.IsType(target.ItemsPanelRoot);
+ var scroll = panel.FindAncestorOfType()!;
+
+ // Select item 1.
+ target.SelectedIndex = 1;
+
+ // Scroll item 1 out of view.
+ scroll.Offset = new(0, 1000);
+ Layout(target);
+
+ Assert.Equal(10, panel.FirstRealizedIndex);
+ Assert.Equal(19, panel.LastRealizedIndex);
+
+ // The selection should be preserved.
+ Assert.Empty(SelectedContainers(target));
+ Assert.Equal(1, target.SelectedIndex);
+ Assert.Same(items[1], target.SelectedItem);
+ Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);
+ Assert.Equal(new[] { items[1] }, target.Selection.SelectedItems);
+ }
+
[Fact]
public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
{
@@ -1197,7 +1281,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Setters =
{
- new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()),
+ new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()),
+ new Setter(TestContainer.HeightProperty, 100.0),
},
};
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index 421ed2c979..70541b6196 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -261,6 +261,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var grid = AvaloniaRuntimeXamlLoader.Parse(xaml);
+ Assert.Equal(4, grid.ColumnDefinitions.Count);
+ Assert.Equal(4, grid.RowDefinitions.Count);
+
+ var expected1 = new GridLength(100);
+ var expected2 = GridLength.Auto;
+ var expected3 = new GridLength(1, GridUnitType.Star);
+ var expected4 = new GridLength(100, GridUnitType.Star);
+
+ Assert.Equal(expected1, grid.ColumnDefinitions[0].Width);
+ Assert.Equal(expected2, grid.ColumnDefinitions[1].Width);
+ Assert.Equal(expected3, grid.ColumnDefinitions[2].Width);
+ Assert.Equal(expected4, grid.ColumnDefinitions[3].Width);
+
+ Assert.Equal(expected1, grid.RowDefinitions[0].Height);
+ Assert.Equal(expected2, grid.RowDefinitions[1].Height);
+ Assert.Equal(expected3, grid.RowDefinitions[2].Height);
+ Assert.Equal(expected4, grid.RowDefinitions[3].Height);
+ }
+
+ [Fact]
+ public void Grid_Row_Col_Definitions_Are_Parsed_Space_Delimiter()
+ {
+ var xaml = @"
+
+";
+
+ var grid = AvaloniaRuntimeXamlLoader.Parse(xaml);
+
+
Assert.Equal(4, grid.ColumnDefinitions.Count);
Assert.Equal(4, grid.RowDefinitions.Count);