From c78528c2fab5ac616193b5944244e831f530a4bf Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Fri, 21 Aug 2026 00:09:21 -0700 Subject: [PATCH 1/3] feat(DialogHost): Allow awaiting visual state transitions Introduces `WaitForOpened` and `WaitForClosed` methods on `DialogHost` to enable asynchronous waiting for the completion of the dialog's visual state transitions. This provides more precise control for executing logic precisely after the dialog has fully opened or closed, rather than just when events are dispatched. Updates the `DialogSession` to expose the parent `DialogHost` instance, facilitating access to these new awaitable methods. --- src/MainDemo.Wpf/Dialogs.xaml.cs | 8 +++- src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs | 9 +++- src/MaterialDesignThemes.Wpf/DialogHost.cs | 40 +++++++++++++---- src/MaterialDesignThemes.Wpf/DialogSession.cs | 24 +++++----- .../Internal/VisualStateMonitor.cs | 45 +++++++++++++++++++ .../MaterialDesignTheme.DialogHost.xaml | 26 +++++------ 6 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs diff --git a/src/MainDemo.Wpf/Dialogs.xaml.cs b/src/MainDemo.Wpf/Dialogs.xaml.cs index f8816bb7df..be028a3edc 100644 --- a/src/MainDemo.Wpf/Dialogs.xaml.cs +++ b/src/MainDemo.Wpf/Dialogs.xaml.cs @@ -42,8 +42,12 @@ private void Sample1_DialogHost_OnDialogClosed(object sender, DialogClosedEventA } // Used for DialogHost.DialogClosingAttached - private void Sample2_DialogHost_OnDialogClosing(object sender, DialogClosingEventArgs eventArgs) - => Debug.WriteLine($"SAMPLE 2: Closing dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + private async void Sample2_DialogHost_OnDialogClosing(object sender, DialogClosingEventArgs eventArgs) + { + Debug.WriteLine($"{DateTime.Now.TimeOfDay} SAMPLE 2: Closing dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + await eventArgs.Session.DialogHost.WaitForClosed(); + Debug.WriteLine($"{DateTime.Now.TimeOfDay} SAMPLE 2: Closed dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + } private void Sample2_DialogHost_OnDialogClosed(object sender, DialogClosedEventArgs eventArgs) => Debug.WriteLine($"SAMPLE 2: Closed dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); diff --git a/src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs b/src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs index 454e84665f..7b3adceb30 100644 --- a/src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs +++ b/src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Threading; using MaterialDesign3Demo.Domain; using MaterialDesignThemes.Wpf; @@ -27,8 +28,12 @@ private void Sample1_DialogHost_OnDialogClosing(object sender, DialogClosingEven } // Used for DialogHost.DialogClosingAttached - private void Sample2_DialogHost_OnDialogClosing(object sender, DialogClosingEventArgs eventArgs) - => Debug.WriteLine($"SAMPLE 2: Closing dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + private async void Sample2_DialogHost_OnDialogClosing(object sender, DialogClosingEventArgs eventArgs) + { + Debug.WriteLine($"{DateTime.Now.TimeOfDay} SAMPLE 2: Closing dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + await eventArgs.Session.DialogHost.WaitForClosed(); + Debug.WriteLine($"{DateTime.Now.TimeOfDay} SAMPLE 2: Closed dialog with parameter: {eventArgs.Parameter ?? string.Empty}"); + } private void Sample5_DialogHost_OnDialogClosing(object sender, DialogClosingEventArgs eventArgs) { diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index 99f88d49db..554bf9a657 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -1,9 +1,12 @@ +using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; +using System.Threading; using System.Windows.Data; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Threading; +using MaterialDesignThemes.Wpf.Internal; namespace MaterialDesignThemes.Wpf; @@ -31,13 +34,18 @@ public enum DialogHostOpenDialogCommandDataContextSource [TemplatePart(Name = PopupPartName, Type = typeof(Popup))] [TemplatePart(Name = PopupPartName, Type = typeof(ContentControl))] [TemplatePart(Name = ContentCoverGridName, Type = typeof(Grid))] -[TemplateVisualState(GroupName = "PopupStates", Name = OpenStateName)] -[TemplateVisualState(GroupName = "PopupStates", Name = ClosedStateName)] +[TemplatePart(Name = RootContentPartName, Type = typeof(FrameworkElement))] +[TemplateVisualState(GroupName = VisualStateGroupName, Name = OpenStateName)] +[TemplateVisualState(GroupName = VisualStateGroupName, Name = ClosedStateName)] public class DialogHost : ContentControl { + public const string VisualStateGroupName = "PopupStates"; + public const string PopupPartName = "PART_Popup"; public const string PopupContentPartName = "PART_PopupContentElement"; public const string ContentCoverGridName = "PART_ContentCoverGrid"; + public const string RootContentPartName = "PART_DialogHostRoot"; + public const string OpenStateName = "Open"; public const string ClosedStateName = "Closed"; @@ -57,6 +65,9 @@ public class DialogHost : ContentControl private DialogClosedEventHandler? _asyncShowClosedEventHandler; private TaskCompletionSource? _dialogTaskCompletionSource; + private VisualStateMonitor? _visualStateMonitor; + + private Popup? _popup; private ContentControl? _popupContentControl; private Grid? _contentCoverGrid; @@ -395,7 +406,8 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj //https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/187 //totally not happy about this, but on immediate validation we can get some weird looking stuff...give WPF a kick to refresh... - Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => { + Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => + { CommandManager.InvalidateRequerySuggested(); //Delay focusing the popup until after the animation has some time, Issue #2912 UIElement? child = dialogHost.FocusPopup(); @@ -628,9 +640,26 @@ public override void OnApplyTemplate() VisualStateManager.GoToState(this, GetStateName(), false); + if (GetTemplateChild(RootContentPartName) is FrameworkElement root && + VisualStateManager.GetVisualStateGroups(root) is [VisualStateGroup stateGroup, ..]) + { + var stateNames = stateGroup.States.OfType().Select(x => x.Name).ToList(); + if (stateNames.Contains(OpenStateName) && stateNames.Contains(ClosedStateName)) + { + _visualStateMonitor = new(stateGroup); + } + } base.OnApplyTemplate(); } + public Task WaitForOpened(CancellationToken cancellationToken = default) + => _visualStateMonitor?.WaitForState(OpenStateName, cancellationToken) + ?? throw new InvalidOperationException("Unable to locate visual states for the DialogHost, cannot wait for state transitions"); + + public Task WaitForClosed(CancellationToken cancellationToken = default) + => _visualStateMonitor?.WaitForState(ClosedStateName, cancellationToken) + ?? throw new InvalidOperationException("Unable to locate visual states for the DialogHost, cannot wait for state transitions"); + #region restore focus properties public static readonly DependencyProperty RestoreFocusElementProperty = DependencyProperty.RegisterAttached( @@ -967,11 +996,6 @@ private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) } } - private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) - { - - } - [SecurityCritical] [DllImport("user32.dll", EntryPoint = "SetFocus", SetLastError = true)] private static extern IntPtr SetFocus(IntPtr hWnd); diff --git a/src/MaterialDesignThemes.Wpf/DialogSession.cs b/src/MaterialDesignThemes.Wpf/DialogSession.cs index 7c4aec531f..63374d4d6b 100644 --- a/src/MaterialDesignThemes.Wpf/DialogSession.cs +++ b/src/MaterialDesignThemes.Wpf/DialogSession.cs @@ -7,16 +7,16 @@ namespace MaterialDesignThemes.Wpf; /// public class DialogSession { - private readonly DialogHost _owner; - internal DialogSession(DialogHost owner) - => _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + => DialogHost = owner ?? throw new ArgumentNullException(nameof(owner)); + + public DialogHost DialogHost { get; } /// - /// Indicates if the dialog session has ended. Once ended no further method calls will be permitted. + /// Indicates if the dialog session has ended. Once ended no further method calls will be permitted. /// /// - /// Client code cannot set this directly, this is internally managed. To end the dialog session use . + /// Client code cannot set this directly, this is internally managed. To end the dialog session use . /// public bool IsEnded { get; internal set; } @@ -28,7 +28,7 @@ internal DialogSession(DialogHost owner) /// /// Gets the which is currently displayed, so this could be a view model or a UI element. /// - public object? Content => _owner.DialogContent; + public object? Content => DialogHost.DialogContent; /// /// Update the current content in the dialog. @@ -36,11 +36,11 @@ internal DialogSession(DialogHost owner) /// public void UpdateContent(object? content) { - _owner.AssertTargetableContent(); - _owner.DialogContent = content; - _owner.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => + DialogHost.AssertTargetableContent(); + DialogHost.DialogContent = content; + DialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => { - _owner.FocusPopup(); + DialogHost.FocusPopup(); })); } @@ -52,7 +52,7 @@ public void Close() { if (IsEnded) throw new InvalidOperationException("Dialog session has ended."); - _owner.InternalClose(null); + DialogHost.InternalClose(null); } /// @@ -64,6 +64,6 @@ public void Close(object? parameter) { if (IsEnded) throw new InvalidOperationException("Dialog session has ended."); - _owner.InternalClose(parameter); + DialogHost.InternalClose(parameter); } } diff --git a/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs new file mode 100644 index 0000000000..7a4b07c1d2 --- /dev/null +++ b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs @@ -0,0 +1,45 @@ +using System.Threading; + +namespace MaterialDesignThemes.Wpf.Internal; + +internal sealed class VisualStateMonitor +{ + private readonly VisualStateGroup _visualStateGroup; + + public VisualStateMonitor(VisualStateGroup visualStateGroup) + { + _visualStateGroup = visualStateGroup ?? + throw new ArgumentNullException(nameof(visualStateGroup)); + } + + public Task WaitForState(string state, CancellationToken cancellationToken) + { + string currentState = _visualStateGroup.CurrentState.Name; + if (currentState == state) return Task.CompletedTask; + + TaskCompletionSource tcs = new(); + cancellationToken.Register(() => tcs.TrySetCanceled()); + + EventHandler stateChanged = null!; + stateChanged = (sender, e) => + { + if (e.NewState.Name == state) + { + _visualStateGroup.CurrentStateChanged -= stateChanged; + tcs.TrySetResult(state); + } + }; + + _visualStateGroup.CurrentStateChanged += stateChanged; + + currentState = _visualStateGroup.CurrentState.Name; + if (currentState == state) + { + _visualStateGroup.CurrentStateChanged -= stateChanged; + + return Task.CompletedTask; + } + + return tcs.Task; + } +} diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml index 0261ae11cd..489d77ca0d 100644 --- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml +++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml @@ -25,11 +25,11 @@ - + - + - + @@ -68,7 +68,7 @@ - + @@ -111,7 +111,7 @@ - + - + @@ -152,7 +152,7 @@ @@ -265,11 +265,11 @@ - + - + - + @@ -308,7 +308,7 @@ - + @@ -351,7 +351,7 @@ - + - + Date: Thu, 27 Aug 2026 21:41:22 -0700 Subject: [PATCH 2/3] fix(DialogHost): Correct visual state type for monitoring transitions The `VisualStateMonitor` failed to correctly identify the 'Open' and 'Closed' states due to an incorrect type cast (`OfType`) when retrieving state names. This prevented the `WaitForOpened` and `WaitForClosed` methods from functioning as intended. This change corrects the type to `VisualState` to ensure proper state detection. Adds a new UI test to confirm the `WaitForClosed` method completes reliably. Fixes #4017. --- src/MaterialDesignThemes.Wpf/DialogHost.cs | 2 +- .../WPF/DialogHosts/DialogHostTests.cs | 32 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index 554bf9a657..04bd00e29d 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -643,7 +643,7 @@ public override void OnApplyTemplate() if (GetTemplateChild(RootContentPartName) is FrameworkElement root && VisualStateManager.GetVisualStateGroups(root) is [VisualStateGroup stateGroup, ..]) { - var stateNames = stateGroup.States.OfType().Select(x => x.Name).ToList(); + var stateNames = stateGroup.States.OfType().Select(x => x.Name).ToList(); if (stateNames.Contains(OpenStateName) && stateNames.Contains(ClosedStateName)) { _visualStateMonitor = new(stateGroup); diff --git a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs index 2efd5da30f..358b8a6235 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs @@ -13,6 +13,32 @@ public DialogHostTests() AttachedDebuggerToRemoteProcess = false; } + [Test] + public async Task WaitForClosed_CompletesAfterDialogCloses() + { + var dialogHost = await LoadXaml(""); + + await dialogHost.RemoteExecute(OpenAndWaitForCompletion); + await Assert.That(dialogHost.GetIsOpen()).IsTrue(); + + await dialogHost.RemoteExecute(CloseAndWaitForCompletion); + await Assert.That(dialogHost.GetIsOpen()).IsFalse(); + + static async Task OpenAndWaitForCompletion(DialogHost dialogHost) + { + Task wait = dialogHost.WaitForOpened(); + dialogHost.IsOpen = true; + await wait; + } + + static async Task CloseAndWaitForCompletion(DialogHost dialogHost) + { + Task wait = dialogHost.WaitForClosed(); + dialogHost.IsOpen = false; + await wait; + } + } + [Test] public async Task OnOpenDialog_OverlayCoversContent() { @@ -288,7 +314,7 @@ public async Task CornerRadius_AppliedToContentCoverBorder_WhenSetOnEmbeddedDial await Wait.For(async () => { var contentCoverBorder = await dialogHost.GetElement("ContentCoverBorder"); - + await Assert.That((await contentCoverBorder.GetCornerRadius()).TopLeft).IsEqualTo(1); await Assert.That((await contentCoverBorder.GetCornerRadius()).TopRight).IsEqualTo(2); await Assert.That((await contentCoverBorder.GetCornerRadius()).BottomRight).IsEqualTo(3); @@ -450,7 +476,7 @@ public async Task DialogHost_WithComboBox_CanSelectItem() var comboBox = await dialogHost.GetElement("TargetedPlatformComboBox"); await Task.Delay(500, TestContext.Current!.Execution.CancellationToken); await comboBox.LeftClick(); - + var item = await Wait.For(() => comboBox.GetElement("TargetItem")); await Task.Delay(TimeSpan.FromSeconds(1)); await item.LeftClick(); @@ -507,7 +533,7 @@ public async Task DialogHost_TrapsFocusInsidePopup_WhenTabbing(string dialogHost await Wait.For(async () => await Assert.That(await textBoxOne.GetIsFocused()).IsTrue()); await textBoxOne.SendInput(new KeyboardInput(inputActions)); - + await Wait.For(async () => await Assert.That(await textBoxTwo.GetIsFocused()).IsTrue()); await textBoxTwo.SendInput(new KeyboardInput(inputActions)); From 699472096444da3770f91332ba998ef67d27d636 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Thu, 27 Aug 2026 22:23:48 -0700 Subject: [PATCH 3/3] fix(DialogHost): Improve robustness of visual state wait methods Ensures `WaitForOpened` and `WaitForClosed` handle cancellation tokens correctly by unsubscribing from `VisualStateGroup` events. This prevents potential resource leaks and ensures reliable cancellation. Adds tests to confirm that waiting methods complete immediately if the dialog is already in the target visual state, enhancing predictability. This also validates the overall reliability of asynchronous state transitions. Fixes #4017. --- src/MaterialDesignThemes.Wpf/DialogHost.cs | 39 +++++++++++++++++++ .../Internal/VisualStateMonitor.cs | 16 ++++---- .../WPF/DialogHosts/DialogHostTests.cs | 39 +++++++++++++++++-- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index 04bd00e29d..71bae50614 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -227,6 +227,33 @@ public static void Close(object? dialogIdentifier, object? parameter) /// public static bool IsDialogOpen(object? dialogIdentifier) => GetDialogSession(dialogIdentifier)?.IsEnded == false; + + /// + /// Waits for the DialogHost to move into the Opened visual state. This is useful when you want to ensure that the dialog is fully opened before performing a UI action, such as focusing a control within the dialog. + /// + /// of the instance where the dialog should be closed. Typically this will match an identifier set in XAML. + /// A token to determine how long to wait. + /// A task that completes when the DialogHost has moved into the Opened visual state. + /// Throw when the DialogHost template does not contain the Opened visual state. + public static Task WaitForOpened(object? dialogIdentifier = null, CancellationToken cancellationToken = default) + { + var instance = GetInstance(dialogIdentifier); + return instance.WaitForOpened(cancellationToken); + } + + /// + /// Waits for the DialogHost to move into the Closed visual state. This is useful when you want to ensure that the dialog is fully closed before performing a UI action, such as focusing a control outside of the dialog. + /// + /// of the instance where the dialog should be closed. Typically this will match an identifier set in XAML. + /// A token to determine how long to wait. + /// A task that completes when the DialogHost has moved into the Closed visual state. + /// Throw when the DialogHost template does not contain the Closed visual state. + public static Task WaitForClosed(object? dialogIdentifier = null, CancellationToken cancellationToken = default) + { + var instance = GetInstance(dialogIdentifier); + return instance.WaitForClosed(cancellationToken); + } + private static DialogHost GetInstance(object? dialogIdentifier) { if (LoadedInstances.Count == 0) @@ -652,10 +679,22 @@ public override void OnApplyTemplate() base.OnApplyTemplate(); } + /// + /// Waits for the DialogHost to move into the Opened visual state. This is useful when you want to ensure that the dialog is fully opened before performing a UI action, such as focusing a control within the dialog. + /// + /// A token to determine how long to wait. + /// A task that completes when the DialogHost has moved into the Opened visual state. + /// Throw when the DialogHost template does not contain the Opened visual state. public Task WaitForOpened(CancellationToken cancellationToken = default) => _visualStateMonitor?.WaitForState(OpenStateName, cancellationToken) ?? throw new InvalidOperationException("Unable to locate visual states for the DialogHost, cannot wait for state transitions"); + /// + /// Waits for the DialogHost to move into the Closed visual state. This is useful when you want to ensure that the dialog is fully closed before performing a UI action, such as focusing a control outside of the dialog. + /// + /// A token to determine how long to wait. + /// A task that completes when the DialogHost has moved into the Closed visual state. + /// Throw when the DialogHost template does not contain the Closed visual state. public Task WaitForClosed(CancellationToken cancellationToken = default) => _visualStateMonitor?.WaitForState(ClosedStateName, cancellationToken) ?? throw new InvalidOperationException("Unable to locate visual states for the DialogHost, cannot wait for state transitions"); diff --git a/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs index 7a4b07c1d2..1d45ef5aa6 100644 --- a/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs +++ b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs @@ -2,15 +2,10 @@ namespace MaterialDesignThemes.Wpf.Internal; -internal sealed class VisualStateMonitor +internal sealed class VisualStateMonitor(VisualStateGroup visualStateGroup) { - private readonly VisualStateGroup _visualStateGroup; - - public VisualStateMonitor(VisualStateGroup visualStateGroup) - { - _visualStateGroup = visualStateGroup ?? + private readonly VisualStateGroup _visualStateGroup = visualStateGroup ?? throw new ArgumentNullException(nameof(visualStateGroup)); - } public Task WaitForState(string state, CancellationToken cancellationToken) { @@ -18,7 +13,6 @@ public Task WaitForState(string state, CancellationToken cancellationToken) if (currentState == state) return Task.CompletedTask; TaskCompletionSource tcs = new(); - cancellationToken.Register(() => tcs.TrySetCanceled()); EventHandler stateChanged = null!; stateChanged = (sender, e) => @@ -30,6 +24,12 @@ public Task WaitForState(string state, CancellationToken cancellationToken) } }; + cancellationToken.Register(() => + { + _visualStateGroup.CurrentStateChanged -= stateChanged; + tcs.TrySetCanceled(cancellationToken); + }); + _visualStateGroup.CurrentStateChanged += stateChanged; currentState = _visualStateGroup.CurrentState.Name; diff --git a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs index 358b8a6235..c6f7630817 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Threading; using System.Windows.Media; using MaterialDesignThemes.UITests.Samples.DialogHost; @@ -14,7 +15,7 @@ public DialogHostTests() } [Test] - public async Task WaitForClosed_CompletesAfterDialogCloses() + public async Task WaitForOpenAndClosed_CompletesAfterDialogAnimates() { var dialogHost = await LoadXaml(""); @@ -26,19 +27,51 @@ public async Task WaitForClosed_CompletesAfterDialogCloses() static async Task OpenAndWaitForCompletion(DialogHost dialogHost) { - Task wait = dialogHost.WaitForOpened(); + CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + Task wait = dialogHost.WaitForOpened(cts.Token); dialogHost.IsOpen = true; await wait; } static async Task CloseAndWaitForCompletion(DialogHost dialogHost) { - Task wait = dialogHost.WaitForClosed(); + CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + Task wait = dialogHost.WaitForClosed(cts.Token); dialogHost.IsOpen = false; await wait; } } + [Test] + public async Task WaitAlreadyReachedState_CompletesImmediately() + { + var dialogHost = await LoadXaml(""); + + await dialogHost.RemoteExecute(OpenAndWaitForCompletionTwice); + await Assert.That(dialogHost.GetIsOpen()).IsTrue(); + + await dialogHost.RemoteExecute(CloseAndWaitForCompletionTwice); + await Assert.That(dialogHost.GetIsOpen()).IsFalse(); + + static async Task OpenAndWaitForCompletionTwice(DialogHost dialogHost) + { + CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + Task wait = dialogHost.WaitForOpened(cts.Token); + dialogHost.IsOpen = true; + await wait; + await dialogHost.WaitForOpened(cts.Token); + } + + static async Task CloseAndWaitForCompletionTwice(DialogHost dialogHost) + { + CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + Task wait = dialogHost.WaitForClosed(cts.Token); + dialogHost.IsOpen = false; + await wait; + await dialogHost.WaitForClosed(cts.Token); + } + } + [Test] public async Task OnOpenDialog_OverlayCoversContent() {