diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
index 0bfc713ba0..397826df53 100644
--- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
+++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
@@ -9,6 +9,16 @@ namespace Avalonia.Threading
///
public class AvaloniaScheduler : LocalScheduler
{
+ ///
+ /// Users can schedule actions on the dispatcher thread while being on the correct thread already.
+ /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases.
+ /// To prevent this we are limiting amount of reentrant calls to before we will
+ /// schedule on a dispatcher anyway.
+ ///
+ private const int MaxReentrantSchedules = 32;
+
+ private int _reentrancyGuard;
+
///
/// The instance of the .
///
@@ -24,31 +34,58 @@ namespace Avalonia.Threading
///
public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action)
{
- var composite = new CompositeDisposable(2);
+ IDisposable PostOnDispatcher()
+ {
+ var composite = new CompositeDisposable(2);
+
+ var cancellation = new CancellationDisposable();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (!cancellation.Token.IsCancellationRequested)
+ {
+ composite.Add(action(this, state));
+ }
+ }, DispatcherPriority.DataBind);
+
+ composite.Add(cancellation);
+
+ return composite;
+ }
+
if (dueTime == TimeSpan.Zero)
{
if (!Dispatcher.UIThread.CheckAccess())
{
- var cancellation = new CancellationDisposable();
- Dispatcher.UIThread.Post(() =>
- {
- if (!cancellation.Token.IsCancellationRequested)
- {
- composite.Add(action(this, state));
- }
- }, DispatcherPriority.DataBind);
- composite.Add(cancellation);
+ return PostOnDispatcher();
}
else
{
- return action(this, state);
+ if (_reentrancyGuard >= MaxReentrantSchedules)
+ {
+ return PostOnDispatcher();
+ }
+
+ try
+ {
+ _reentrancyGuard++;
+
+ return action(this, state);
+ }
+ finally
+ {
+ _reentrancyGuard--;
+ }
}
}
else
{
+ var composite = new CompositeDisposable(2);
+
composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime));
+
+ return composite;
}
- return composite;
}
}
}
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index b458b15c64..cf0652247f 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -66,6 +66,7 @@ namespace Avalonia.Controls
static ToolTip()
{
TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged);
+ IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged);
IsOpenProperty.Changed.Subscribe(IsOpenChanged);
}
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index 569697304f..e2a0f9e50c 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -28,20 +28,33 @@ namespace Avalonia.Controls
{
control.PointerEnter -= ControlPointerEnter;
control.PointerLeave -= ControlPointerLeave;
- control.DetachedFromVisualTree -= ControlDetaching;
}
if (e.NewValue != null)
{
control.PointerEnter += ControlPointerEnter;
control.PointerLeave += ControlPointerLeave;
+ }
+ }
+
+ internal void TipOpenChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var control = (Control)e.Sender;
+
+ if (e.OldValue is false && e.NewValue is true)
+ {
control.DetachedFromVisualTree += ControlDetaching;
}
+ else if(e.OldValue is true && e.NewValue is false)
+ {
+ control.DetachedFromVisualTree -= ControlDetaching;
+ }
}
private void ControlDetaching(object sender, VisualTreeAttachmentEventArgs e)
{
var control = (Control)sender;
+ control.DetachedFromVisualTree -= ControlDetaching;
Close(control);
}
diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml
index 8ee818bad2..0fd6b144a4 100644
--- a/src/Avalonia.Themes.Default/ComboBox.xaml
+++ b/src/Avalonia.Themes.Default/ComboBox.xaml
@@ -25,7 +25,7 @@
-
+
+ IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.IsNullOrEmpty}}"
+ HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+ VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
+ CaretBrush="{TemplateBinding CaretBrush}"
+ HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+ VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index 9d7bc6af74..e52e7a487b 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Reactive.Disposables;
+using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
@@ -64,6 +65,39 @@ namespace Avalonia.Controls.UnitTests
Assert.False(ToolTip.GetIsOpen(target));
}
}
+
+ [Fact]
+ public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+ window.DataContext = new ToolTipViewModel();
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ var target = window.Find("PART_target");
+ var panel = window.Find("PART_panel");
+
+ Assert.True((target as IVisual).IsAttachedToVisualTree);
+
+ _mouseHelper.Enter(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ panel.Children.Remove(target);
+
+ Assert.False(ToolTip.GetIsOpen(target));
+ }
+ }
[Fact]
public void Should_Open_On_Pointer_Enter()
@@ -208,4 +242,9 @@ namespace Avalonia.Controls.UnitTests
}
}
}
+
+ internal class ToolTipViewModel
+ {
+ public string Tip => "Tip";
+ }
}