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 @@ - + - +