From b560ee784ca411d301ddd1c411354707e83d3a99 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Thu, 3 Sep 2026 22:50:29 -0700 Subject: [PATCH] fix(DialogHost): Improve reliability of visual state monitoring Refactors `VisualStateMonitor` to track states via attached properties that are explicitly updated by XAML Storyboard animations. This change decouples state tracking from potentially inconsistent `VisualStateGroup.CurrentStateChanged` events. By having animations directly update the monitor's `CurrentState` attached property, the monitor now accurately reflects the visual state transitions for `DialogHost`'s open/closed animations. This enhances the robustness and reliability of asynchronous state waiting methods, such as `WaitForOpened` and `WaitForClosed`. Fixes: #4086 --- src/MaterialDesignThemes.Wpf/DialogHost.cs | 9 +-- .../Internal/VisualStateMonitor.cs | 60 ++++++++++++++----- .../MaterialDesignTheme.DialogHost.xaml | 17 ++++++ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index 71bae50614..d947bd10e9 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -667,14 +667,9 @@ public override void OnApplyTemplate() VisualStateManager.GoToState(this, GetStateName(), false); - if (GetTemplateChild(RootContentPartName) is FrameworkElement root && - VisualStateManager.GetVisualStateGroups(root) is [VisualStateGroup stateGroup, ..]) + if (GetTemplateChild(RootContentPartName) is FrameworkElement root) { - var stateNames = stateGroup.States.OfType().Select(x => x.Name).ToList(); - if (stateNames.Contains(OpenStateName) && stateNames.Contains(ClosedStateName)) - { - _visualStateMonitor = new(stateGroup); - } + _visualStateMonitor = new(root); } base.OnApplyTemplate(); } diff --git a/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs index 1d45ef5aa6..224dd9c705 100644 --- a/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs +++ b/src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs @@ -1,41 +1,73 @@ -using System.Threading; +using System.Threading; namespace MaterialDesignThemes.Wpf.Internal; -internal sealed class VisualStateMonitor(VisualStateGroup visualStateGroup) +public sealed class VisualStateMonitor { - private readonly VisualStateGroup _visualStateGroup = visualStateGroup ?? - throw new ArgumentNullException(nameof(visualStateGroup)); + public static VisualStateMonitor? GetMonitor(DependencyObject obj) + => (VisualStateMonitor?)obj.GetValue(MonitorProperty); + + public static void SetMonitor(DependencyObject obj, VisualStateMonitor? value) + => obj.SetValue(MonitorProperty, value); + + public static readonly DependencyProperty MonitorProperty = + DependencyProperty.RegisterAttached("Monitor", typeof(VisualStateMonitor), typeof(VisualStateMonitor), new PropertyMetadata(null)); + + public static string? GetCurrentState(DependencyObject obj) + => (string?)obj.GetValue(CurrentStateProperty); + + public static void SetCurrentState(DependencyObject obj, string? value) + => obj.SetValue(CurrentStateProperty, value); + + public static readonly DependencyProperty CurrentStateProperty = + DependencyProperty.RegisterAttached("CurrentState", typeof(string), typeof(VisualStateMonitor), new PropertyMetadata("", OnCurrentStateChanged)); + + private static void OnCurrentStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (GetMonitor(d) is { } monitor) + { + monitor.StateChanged((string)e.NewValue); + } + } + + private string _currentState = ""; + private event EventHandler? CurrentStateChanged; + + public VisualStateMonitor(DependencyObject source) => SetMonitor(source, this); + + public void StateChanged(string state) + { + _currentState = state; + CurrentStateChanged?.Invoke(this, state); + } public Task WaitForState(string state, CancellationToken cancellationToken) { - string currentState = _visualStateGroup.CurrentState.Name; - if (currentState == state) return Task.CompletedTask; + if (_currentState == state) return Task.CompletedTask; TaskCompletionSource tcs = new(); - EventHandler stateChanged = null!; + EventHandler stateChanged = null!; stateChanged = (sender, e) => { - if (e.NewState.Name == state) + if (e == state) { - _visualStateGroup.CurrentStateChanged -= stateChanged; + CurrentStateChanged -= stateChanged; tcs.TrySetResult(state); } }; cancellationToken.Register(() => { - _visualStateGroup.CurrentStateChanged -= stateChanged; + CurrentStateChanged -= stateChanged; tcs.TrySetCanceled(cancellationToken); }); - _visualStateGroup.CurrentStateChanged += stateChanged; + CurrentStateChanged += stateChanged; - currentState = _visualStateGroup.CurrentState.Name; - if (currentState == state) + if (_currentState == state) { - _visualStateGroup.CurrentStateChanged -= stateChanged; + CurrentStateChanged -= stateChanged; return Task.CompletedTask; } diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml index 489d77ca0d..e345053d26 100644 --- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml +++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.DialogHost.xaml @@ -1,6 +1,7 @@  @@ -66,6 +67,10 @@ + + + @@ -108,6 +113,10 @@ + + + @@ -134,6 +143,10 @@ Storyboard.TargetProperty="ScaleY" To="1" Duration="0" /> + + + @@ -145,6 +158,10 @@ Storyboard.TargetProperty="Opacity" To="0" Duration="0" /> + + +