Browse Source

Fix TabControl crash when rapidly switching tabs during transitions (#21227)

* Fix TabControl crash when switching back during tab transition

* More changes
release/latest
Javier Suárez 5 months ago
committed by Julien Lebosquain
parent
commit
3f0e181f39
  1. 67
      src/Avalonia.Controls/TabControl.cs
  2. 168
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs

67
src/Avalonia.Controls/TabControl.cs

@ -254,13 +254,7 @@ namespace Avalonia.Controls
_currentTransition = null;
_shouldAnimate = false;
if (_contentPresenter2 is { IsVisible: true })
{
_contentPresenter2.IsVisible = false;
_contentPresenter2.Content = null;
_contentPresenter2.ContentTemplate = null;
_contentPresenter2.DataContext = null;
}
ResetTransitionPresenter(_contentPresenter2);
int oldIndex = _previousSelectedIndex;
_previousSelectedIndex = SelectedIndex;
@ -306,8 +300,8 @@ namespace Avalonia.Controls
_contentPresenter2!.ContentTemplate = template;
_contentPresenter2!.IsVisible = true;
if (content is Control && template is null)
_contentPresenter2.SetContentWithDataContext(content, container.DataContext);
if (content is Control control && template is null)
SetControlContent(_contentPresenter2, control, container.DataContext);
else
_contentPresenter2.Content = content;
@ -320,8 +314,8 @@ namespace Avalonia.Controls
if (ContentPart != null)
{
var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty));
if (content is Control && template is null)
ContentPart.SetContentWithDataContext(content, container.DataContext);
if (content is Control control && template is null)
SetControlContent(ContentPart, control, container.DataContext);
else
ContentPart.Content = content;
}
@ -408,6 +402,7 @@ namespace Avalonia.Controls
_currentTransition?.Dispose();
_currentTransition = null;
_shouldAnimate = false;
ResetTransitionPresenter(_contentPresenter2);
}
protected override Size ArrangeOverride(Size finalSize)
@ -496,6 +491,56 @@ namespace Avalonia.Controls
(_contentPart, _contentPresenter2) = (_contentPresenter2, _contentPart);
}
private void SetControlContent(ContentPresenter presenter, Control content, object? dataContext)
{
ClearOwningContentPresenter(presenter, content);
ClearPresenterContent(ContentPart, presenter, content);
ClearPresenterContent(_contentPresenter2, presenter, content);
presenter.SetContentWithDataContext(content, dataContext);
}
private void ClearOwningContentPresenter(ContentPresenter targetPresenter, Control content)
{
if (content.VisualParent is ContentPresenter parent &&
!ReferenceEquals(parent, targetPresenter) &&
ReferenceEquals(parent.Host, this) &&
ReferenceEquals(parent.Content, content))
{
ClearPresenterContent(parent, content);
}
}
private static void ClearPresenterContent(ContentPresenter? presenter, ContentPresenter targetPresenter, Control content)
{
if (!ReferenceEquals(presenter, targetPresenter))
ClearPresenterContent(presenter, content);
}
private static void ClearPresenterContent(ContentPresenter? presenter, Control content)
{
if (presenter != null && ReferenceEquals(presenter.Content, content))
{
presenter.Content = null;
presenter.ContentTemplate = null;
presenter.DataContext = null;
presenter.RenderTransform = null;
presenter.Opacity = 1;
}
}
private static void ResetTransitionPresenter(ContentPresenter? presenter)
{
if (presenter == null)
return;
presenter.IsVisible = false;
presenter.Content = null;
presenter.ContentTemplate = null;
presenter.DataContext = null;
presenter.RenderTransform = null;
presenter.Opacity = 1;
}
private void UpdateTabStripPlacement()
{
var controls = ItemsPresenterPart?.Panel?.Children;

168
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -21,6 +22,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
@ -913,6 +915,165 @@ namespace Avalonia.Controls.UnitTests
Times.Once);
}
[Fact]
public void Interrupted_PageTransition_Can_Select_Original_Control_Before_Previous_Transition_Completes()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var starts = new List<(object? FromContent, object? ToContent, bool Forward)>();
var transitionGate = new TaskCompletionSource();
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Callback<Visual?, Visual?, bool, CancellationToken>((from, to, forward, _) =>
{
starts.Add((
(from as ContentPresenter)?.Content,
(to as ContentPresenter)?.Content,
forward));
})
.Returns(transitionGate.Task);
var target = new TabControl
{
PageTransition = transition.Object,
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
root.LayoutManager.ExecuteLayoutPass();
Assert.Single(starts);
Assert.Same(firstPage, starts[0].FromContent);
Assert.Same(secondPage, starts[0].ToContent);
Assert.True(starts[0].Forward);
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(2, starts.Count);
Assert.Null(starts[1].FromContent);
Assert.Same(firstPage, starts[1].ToContent);
Assert.False(starts[1].Forward);
Assert.Same(firstPage, target.SelectedContent);
}
[Fact]
public void Pending_PageTransition_Can_Select_Original_Control_Before_Transition_Starts()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var starts = new List<(object? FromContent, object? ToContent, bool Forward)>();
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Callback<Visual?, Visual?, bool, CancellationToken>((from, to, forward, _) =>
{
starts.Add((
(from as ContentPresenter)?.Content,
(to as ContentPresenter)?.Content,
forward));
})
.Returns(Task.CompletedTask);
var target = new TabControl
{
PageTransition = transition.Object,
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
root.LayoutManager.ExecuteLayoutPass();
Assert.Single(starts);
Assert.Null(starts[0].FromContent);
Assert.Same(firstPage, starts[0].ToContent);
Assert.False(starts[0].Forward);
Assert.Same(firstPage, target.SelectedContent);
}
[Fact]
public void Interrupted_PageTransition_Clears_Reused_Control_From_Owning_SelectedContentHost()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
var target = new TabControl
{
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
root.LayoutManager.ExecuteLayoutPass();
var primary = target.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(x => x.Name == "PART_SelectedContentHost");
var secondary = target.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(x => x.Name == "PART_SelectedContentHost2");
// Simulate the stale presenter ownership that can happen when tab changes
// interrupt a transition: the page is still parented by the named content
// host, but the active field no longer points at that host.
primary.SetContentWithDataContext(firstPage, null);
secondary.IsVisible = false;
SetPrivateField(target, "_contentPart", secondary);
SetPrivateField(target, "_contentPresenter2", secondary);
target.PageTransition = transition.Object;
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
Assert.Same(firstPage, target.SelectedContent);
Assert.Null(primary.Content);
Assert.Same(firstPage, secondary.Content);
}
private static IControlTemplate TabControlTemplate()
{
return new FuncControlTemplate<TabControl>((parent, scope) =>
@ -943,6 +1104,13 @@ namespace Avalonia.Controls.UnitTests
});
}
private static void SetPrivateField<T>(TabControl target, string name, T value)
{
var field = typeof(TabControl).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(field);
field.SetValue(target, value);
}
private static IControlTemplate TabItemTemplate()
{
return new FuncControlTemplate<TabItem>((parent, scope) =>

Loading…
Cancel
Save