Skip to content
Draft
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
40 changes: 32 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 @@ -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();
Expand Down Expand Up @@ -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<VisualStateGroup>().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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding whether cancellationToken should be required: How about setting a reasonable self-cancelling token (say 5 seconds) if one is not supplied by the caller? I guess what you're trying to avoid by making it mandatory is the infinite hang waiting for a state change that never occurs? A reasonable default token would do that too I guess.

My assumption here is that the caller would only call these methods right before opening/closing the DialogHost and therefore the timeout should just be slightly longer than the duration of the animation. I hope my assumption is correct.

?? 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(
Expand Down Expand Up @@ -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);
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
{
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<string> tcs = new();
cancellationToken.Register(() => tcs.TrySetCanceled());

EventHandler<VisualStateChangedEventArgs> 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;
}
Comment on lines +35 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you already have this guard in line 17-18? Is there a scenario where the current state can change between line 18 and here?


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