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 @@
WinExenet6.0true
+ true
@@ -17,4 +18,5 @@
+
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index 57ed67b508..f2b105c901 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
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/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/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..67ec238ceb 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)
@@ -578,7 +587,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/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index d640a0c2dc..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;
@@ -237,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">
+ {
+ 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/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..928c0c94ef 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
@@ -1024,6 +1024,56 @@ 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_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
{
@@ -1197,7 +1247,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Setters =
{
- new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()),
+ new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()),
},
};
}