Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/MainDemo.Wpf/Dialogs.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
9 changes: 7 additions & 2 deletions src/MaterialDesign3.Demo.Wpf/Dialogs.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Threading;
using MaterialDesign3Demo.Domain;
using MaterialDesignThemes.Wpf;

Expand Down Expand Up @@ -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)
{
Expand Down
79 changes: 71 additions & 8 deletions src/MaterialDesignThemes.Wpf/DialogHost.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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";

Expand All @@ -57,6 +65,9 @@ public class DialogHost : ContentControl
private DialogClosedEventHandler? _asyncShowClosedEventHandler;
private TaskCompletionSource<object?>? _dialogTaskCompletionSource;

private VisualStateMonitor? _visualStateMonitor;


private Popup? _popup;
private ContentControl? _popupContentControl;
private Grid? _contentCoverGrid;
Expand Down Expand Up @@ -216,6 +227,33 @@ public static void Close(object? dialogIdentifier, object? parameter)
/// <returns></returns>
public static bool IsDialogOpen(object? dialogIdentifier) => GetDialogSession(dialogIdentifier)?.IsEnded == false;


/// <summary>
/// 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.
/// </summary>
/// <param name="dialogIdentifier">of the instance where the dialog should be closed. Typically this will match an identifier set in XAML.</param>
/// <param name="cancellationToken">A token to determine how long to wait.</param>
/// <returns>A task that completes when the DialogHost has moved into the Opened visual state.</returns>
/// <exception cref="InvalidOperationException">Throw when the DialogHost template does not contain the Opened visual state.</exception>
public static Task WaitForOpened(object? dialogIdentifier = null, CancellationToken cancellationToken = default)
{
var instance = GetInstance(dialogIdentifier);
return instance.WaitForOpened(cancellationToken);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="dialogIdentifier">of the instance where the dialog should be closed. Typically this will match an identifier set in XAML.</param>
/// <param name="cancellationToken">A token to determine how long to wait.</param>
/// <returns>A task that completes when the DialogHost has moved into the Closed visual state.</returns>
/// <exception cref="InvalidOperationException">Throw when the DialogHost template does not contain the Closed visual state.</exception>
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)
Expand Down Expand Up @@ -395,7 +433,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();
Expand Down Expand Up @@ -628,9 +667,38 @@ 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<VisualState>().Select(x => x.Name).ToList();
if (stateNames.Contains(OpenStateName) && stateNames.Contains(ClosedStateName))
{
_visualStateMonitor = new(stateGroup);
}
}
base.OnApplyTemplate();
}

/// <summary>
/// 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.
/// </summary>
/// <param name="cancellationToken">A token to determine how long to wait.</param>
/// <returns>A task that completes when the DialogHost has moved into the Opened visual state.</returns>
/// <exception cref="InvalidOperationException">Throw when the DialogHost template does not contain the Opened visual state.</exception>
public Task WaitForOpened(CancellationToken cancellationToken = default)
=> _visualStateMonitor?.WaitForState(OpenStateName, cancellationToken)
Comment thread
Keboo marked this conversation as resolved.
?? throw new InvalidOperationException("Unable to locate visual states for the DialogHost, cannot wait for state transitions");

/// <summary>
/// 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.
/// </summary>
/// <param name="cancellationToken">A token to determine how long to wait.</param>
/// <returns>A task that completes when the DialogHost has moved into the Closed visual state.</returns>
/// <exception cref="InvalidOperationException">Throw when the DialogHost template does not contain the Closed visual state.</exception>
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(
Expand Down Expand Up @@ -967,11 +1035,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);
Expand Down
24 changes: 12 additions & 12 deletions src/MaterialDesignThemes.Wpf/DialogSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ namespace MaterialDesignThemes.Wpf;
/// </summary>
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; }

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Client code cannot set this directly, this is internally managed. To end the dialog session use <see cref="Close()"/>.
/// Client code cannot set this directly, this is internally managed. To end the dialog session use <see cref="Close()"/>.
/// </remarks>
public bool IsEnded { get; internal set; }

Expand All @@ -28,19 +28,19 @@ internal DialogSession(DialogHost owner)
/// <summary>
/// Gets the <see cref="DialogHost.DialogContent"/> which is currently displayed, so this could be a view model or a UI element.
/// </summary>
public object? Content => _owner.DialogContent;
public object? Content => DialogHost.DialogContent;

/// <summary>
/// Update the current content in the dialog.
/// </summary>
/// <param name="content"></param>
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();
}));
}

Expand All @@ -52,7 +52,7 @@ public void Close()
{
if (IsEnded) throw new InvalidOperationException("Dialog session has ended.");

_owner.InternalClose(null);
DialogHost.InternalClose(null);
}

/// <summary>
Expand All @@ -64,6 +64,6 @@ public void Close(object? parameter)
{
if (IsEnded) throw new InvalidOperationException("Dialog session has ended.");

_owner.InternalClose(parameter);
DialogHost.InternalClose(parameter);
}
}
45 changes: 45 additions & 0 deletions src/MaterialDesignThemes.Wpf/Internal/VisualStateMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Threading;

namespace MaterialDesignThemes.Wpf.Internal;

internal sealed class VisualStateMonitor(VisualStateGroup visualStateGroup)
{
private readonly 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<string> tcs = new();

EventHandler<VisualStateChangedEventArgs> stateChanged = null!;
stateChanged = (sender, e) =>
{
if (e.NewState.Name == state)
{
_visualStateGroup.CurrentStateChanged -= stateChanged;
tcs.TrySetResult(state);
}
};

cancellationToken.Register(() =>
{
_visualStateGroup.CurrentStateChanged -= stateChanged;
tcs.TrySetCanceled(cancellationToken);
});

_visualStateGroup.CurrentStateChanged += stateChanged;

currentState = _visualStateGroup.CurrentState.Name;
if (currentState == state)
{
_visualStateGroup.CurrentStateChanged -= stateChanged;

return Task.CompletedTask;
}
Comment thread
Keboo marked this conversation as resolved.

return tcs.Task;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
<ControlTemplate.Resources>
<converters:FirstNonNullConverter x:Key="FirstNonNullConverter" />
</ControlTemplate.Resources>
<Grid x:Name="DialogHostRoot" Focusable="False">
<Grid x:Name="PART_DialogHostRoot" Focusable="False">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PopupStates">
<VisualStateGroup Name="{x:Static wpf:DialogHost.VisualStateGroupName}">
<VisualStateGroup.Transitions>
<VisualTransition From="Closed" To="Open">
<VisualTransition From="{x:Static wpf:DialogHost.ClosedStateName}" To="{x:Static wpf:DialogHost.OpenStateName}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup" Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
Expand Down Expand Up @@ -68,7 +68,7 @@
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="Open" To="Closed">
<VisualTransition From="{x:Static wpf:DialogHost.OpenStateName}" To="{x:Static wpf:DialogHost.ClosedStateName}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup" Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.3" Value="False" />
Expand Down Expand Up @@ -111,7 +111,7 @@
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Open">
<VisualState Name="{x:Static wpf:DialogHost.OpenStateName}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup"
Storyboard.TargetProperty="IsOpen"
Expand All @@ -136,7 +136,7 @@
Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Closed">
<VisualState Name="{x:Static wpf:DialogHost.ClosedStateName}">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup" Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0" Value="False" />
Expand All @@ -152,7 +152,7 @@
<wpf:PopupEx x:Name="PART_Popup"
wpf:ThemeAssist.Theme="{TemplateBinding DialogTheme}"
Placement="{TemplateBinding Placement}"
PlacementTarget="{Binding ElementName=DialogHostRoot, Mode=OneWay}"
PlacementTarget="{Binding ElementName=PART_DialogHostRoot, Mode=OneWay}"
Style="{TemplateBinding PopupStyle}">
<Grid>
<Border Background="Transparent" IsHitTestVisible="{TemplateBinding CloseOnClickAway}">
Expand Down Expand Up @@ -265,11 +265,11 @@
<ControlTemplate.Resources>
<converters:FirstNonNullConverter x:Key="FirstNonNullConverter" />
</ControlTemplate.Resources>
<Grid x:Name="DialogHostRoot" Focusable="False">
<Grid x:Name="PART_DialogHostRoot" Focusable="False">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PopupStates">
<VisualStateGroup Name="{x:Static wpf:DialogHost.VisualStateGroupName}">
<VisualStateGroup.Transitions>
<VisualTransition From="Closed" To="Open">
<VisualTransition From="{x:Static wpf:DialogHost.ClosedStateName}" To="{x:Static wpf:DialogHost.OpenStateName}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
Expand Down Expand Up @@ -308,7 +308,7 @@
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="Open" To="Closed">
<VisualTransition From="{x:Static wpf:DialogHost.OpenStateName}" To="{x:Static wpf:DialogHost.ClosedStateName}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" />
Expand Down Expand Up @@ -351,7 +351,7 @@
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Open">
<VisualState Name="{x:Static wpf:DialogHost.OpenStateName}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup"
Storyboard.TargetProperty="Visibility"
Expand All @@ -376,7 +376,7 @@
Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Closed">
<VisualState Name="{x:Static wpf:DialogHost.ClosedStateName}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Popup"
Storyboard.TargetProperty="Visibility"
Expand Down
Loading
Loading