Skip to content

Commit 020cea3

Browse files
authored
Closing Dialog with IsOpen property now raises Closing event (#2292)
Fixes #2282
1 parent a12e00b commit 020cea3

File tree

9 files changed

+158
-56
lines changed

9 files changed

+158
-56
lines changed

Directory.packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
<PackageVersion Include="Microsoft.Composition" Version="1.0.31" />
1313
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
1414
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="5.0.0" />
15-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
15+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
16+
<PackageVersion Include="Microsoft.Toolkit.MVVM" Version="7.0.0" />
1617
<PackageVersion Include="Newtonsoft.Json" Version="12.0.3" />
1718
<PackageVersion Include="Shouldly" Version="3.0.2" />
1819
<PackageVersion Include="ShowMeTheXAML" Version="2.0.0" />

MainDemo.Wpf/Domain/Sample4Dialog.xaml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
66
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
7-
mc:Ignorable="d" >
7+
xmlns:domain="clr-namespace:MaterialDesignDemo.Domain"
8+
d:DataContext="{d:DesignInstance Type=domain:DialogsViewModel}"
9+
mc:Ignorable="d">
810
<Grid Margin="16">
911
<Grid.RowDefinitions>
1012
<RowDefinition />
1113
<RowDefinition />
1214
</Grid.RowDefinitions>
13-
<TextBox wpf:HintAssist.Hint="Name" Style="{DynamicResource MaterialDesignFloatingHintTextBox}"
15+
<TextBox wpf:HintAssist.Hint="Name"
16+
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
1417
Margin="0 6 0 0"
1518
FontSize="18" Grid.Row="0"
1619
/>
1720
<StackPanel Orientation="Horizontal" Grid.Row="1"
1821
Margin="0 16 0 0">
19-
<Button IsDefault="True" Style="{DynamicResource MaterialDesignFlatButton}"
22+
<Button IsDefault="True"
23+
Style="{StaticResource MaterialDesignFlatButton}"
2024
Command="{Binding AcceptSample4DialogCommand}">
2125
ACCEPT
2226
</Button>
23-
<Button IsCancel="True" Margin="8 0 0 0" Style="{DynamicResource MaterialDesignFlatButton}"
27+
<Button IsCancel="True"
28+
Margin="8 0 0 0"
29+
Style="{StaticResource MaterialDesignFlatButton}"
2430
Command="{Binding CancelSample4DialogCommand}">
2531
CANCEL
2632
</Button>

MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" />
11+
<PackageReference Include="Microsoft.Toolkit.MVVM" />
1112
<PackageReference Include="XAMLTest" />
1213
<PackageReference Include="xunit" />
1314
<PackageReference Include="xunit.runner.visualstudio">
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.ClosingEventCounter"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
7+
xmlns:dialogHost="clr-namespace:MaterialDesignThemes.UITests.Samples.DialogHost"
8+
d:DataContext="{d:DesignInstance Type=dialogHost:ClosingEventViewModel}"
9+
mc:Ignorable="d"
10+
d:DesignHeight="450" d:DesignWidth="800">
11+
<UserControl.DataContext>
12+
<dialogHost:ClosingEventViewModel />
13+
</UserControl.DataContext>
14+
<materialDesign:DialogHost IsOpen="{Binding DialogIsOpen}" DialogClosing="DialogHost_DialogClosing">
15+
<materialDesign:DialogHost.DialogContent>
16+
<StackPanel Margin="10">
17+
<TextBlock Text="Some Text" />
18+
<Button Content="Close"
19+
Command="{Binding CloseDialogCommand}"
20+
x:Name="CloseButton"/>
21+
</StackPanel>
22+
</materialDesign:DialogHost.DialogContent>
23+
24+
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Left">
25+
<Button Content="Show Dialog"
26+
x:Name="ShowDialogButton"
27+
Command="{Binding OpenDialogCommand}"/>
28+
<TextBlock x:Name="ResultTextBlock" Text="0" />
29+
</StackPanel>
30+
</materialDesign:DialogHost>
31+
</UserControl>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.ComponentModel;
2+
using System.Windows.Controls;
3+
4+
namespace MaterialDesignThemes.UITests.Samples.DialogHost
5+
{
6+
/// <summary>
7+
/// Interaction logic for ClosingEventCounter.xaml
8+
/// </summary>
9+
public partial class ClosingEventCounter : UserControl
10+
{
11+
public ClosingEventCounter()
12+
=> InitializeComponent();
13+
14+
private int _ClosingCount;
15+
private void DialogHost_DialogClosing(object sender, Wpf.DialogClosingEventArgs eventArgs)
16+
=> ResultTextBlock.Text = (++_ClosingCount).ToString();
17+
}
18+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel;
3+
using System.Runtime.CompilerServices;
4+
using System.Windows.Input;
5+
using Microsoft.Toolkit.Mvvm.Input;
6+
7+
namespace MaterialDesignThemes.UITests.Samples.DialogHost
8+
{
9+
public class ClosingEventViewModel : INotifyPropertyChanged
10+
{
11+
public event PropertyChangedEventHandler? PropertyChanged;
12+
13+
private bool _dialogIsOpen;
14+
public bool DialogIsOpen
15+
{
16+
get => _dialogIsOpen;
17+
set => SetProperty(ref _dialogIsOpen, value);
18+
}
19+
20+
private ICommand? _closeDialogCommand;
21+
public ICommand CloseDialogCommand => _closeDialogCommand ??= new RelayCommand(CloseDialog);
22+
23+
private ICommand? _openDialogCommand;
24+
public ICommand OpenDialogCommand => _openDialogCommand ??= new RelayCommand(OpenDialog);
25+
26+
private void OpenDialog()
27+
=> DialogIsOpen = true;
28+
29+
private void CloseDialog()
30+
=> DialogIsOpen = false;
31+
32+
private bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
33+
{
34+
if (!EqualityComparer<T>.Default.Equals(field, newValue))
35+
{
36+
field = (newValue);
37+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
38+
return true;
39+
}
40+
41+
return false;
42+
}
43+
}
44+
}

MaterialDesignThemes.UITests/WPF/DialogHost/DialogHostTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.ComponentModel;
23
using System.Threading.Tasks;
34
using System.Windows;
45
using MaterialDesignThemes.UITests.Samples.DialogHost;
6+
using MaterialDesignThemes.UITests.WPF.TimePicker;
57
using XamlTest;
68
using Xunit;
79
using Xunit.Abstractions;
@@ -44,5 +46,24 @@ public async Task OnOpenDialog_OverlayCoversContent()
4446
await testOverlayButton.Click();
4547
await Wait.For(async () => Assert.Equal("Clicks: 2", await resultTextBlock.GetText()), retry);
4648
}
49+
50+
[Fact]
51+
[Description("Issue 2282")]
52+
public async Task ClosingDialogWithIsOpenProperty_ShouldRaiseDialogClosingEvent()
53+
{
54+
await using var recorder = new TestRecorder(App);
55+
56+
IVisualElement dialogHost = await LoadUserControl<ClosingEventCounter>();
57+
IVisualElement showButton = await dialogHost.GetElement("ShowDialogButton");
58+
IVisualElement closeButton = await dialogHost.GetElement("CloseButton");
59+
IVisualElement resultTextBlock = await dialogHost.GetElement("ResultTextBlock");
60+
61+
62+
await showButton.Click();
63+
await Wait.For(async () => await closeButton.GetIsVisible());
64+
await closeButton.Click();
65+
66+
await Wait.For(async () => Assert.Equal("1", await resultTextBlock.GetText()));
67+
}
4768
}
4869
}

MaterialDesignThemes.Wpf/DialogHost.cs

Lines changed: 30 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Threading.Tasks;
@@ -285,8 +285,17 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
285285
object? closeParameter = null;
286286
if (dialogHost.CurrentSession is { } session)
287287
{
288+
if (!session.IsEnded)
289+
{
290+
session.Close(session.CloseParameter);
291+
}
292+
//DialogSession.Close may attempt to cancel the closing of the dialog.
293+
//When the dialog is closed in this manner it is not valid
294+
if (!session.IsEnded)
295+
{
296+
throw new InvalidOperationException($"Cannot cancel dialog closing after {nameof(IsOpen)} property has been set to {bool.FalseString}");
297+
}
288298
closeParameter = session.CloseParameter;
289-
session.IsEnded = true;
290299
dialogHost.CurrentSession = null;
291300
}
292301

@@ -524,14 +533,10 @@ public event DialogOpenedEventHandler DialogOpened
524533
"DialogOpenedAttached", typeof(DialogOpenedEventHandler), typeof(DialogHost), new PropertyMetadata(default(DialogOpenedEventHandler)));
525534

526535
public static void SetDialogOpenedAttached(DependencyObject element, DialogOpenedEventHandler value)
527-
{
528-
element.SetValue(DialogOpenedAttachedProperty, value);
529-
}
536+
=> element.SetValue(DialogOpenedAttachedProperty, value);
530537

531538
public static DialogOpenedEventHandler GetDialogOpenedAttached(DependencyObject element)
532-
{
533-
return (DialogOpenedEventHandler)element.GetValue(DialogOpenedAttachedProperty);
534-
}
539+
=> (DialogOpenedEventHandler)element.GetValue(DialogOpenedAttachedProperty);
535540

536541
public static readonly DependencyProperty DialogOpenedCallbackProperty = DependencyProperty.Register(
537542
nameof(DialogOpenedCallback), typeof(DialogOpenedEventHandler), typeof(DialogHost), new PropertyMetadata(default(DialogOpenedEventHandler)));
@@ -546,9 +551,7 @@ public DialogOpenedEventHandler? DialogOpenedCallback
546551
}
547552

548553
protected void OnDialogOpened(DialogOpenedEventArgs eventArgs)
549-
{
550-
RaiseEvent(eventArgs);
551-
}
554+
=> RaiseEvent(eventArgs);
552555

553556
#endregion
554557

@@ -577,14 +580,10 @@ public event DialogClosingEventHandler DialogClosing
577580
"DialogClosingAttached", typeof(DialogClosingEventHandler), typeof(DialogHost), new PropertyMetadata(default(DialogClosingEventHandler)));
578581

579582
public static void SetDialogClosingAttached(DependencyObject element, DialogClosingEventHandler value)
580-
{
581-
element.SetValue(DialogClosingAttachedProperty, value);
582-
}
583+
=> element.SetValue(DialogClosingAttachedProperty, value);
583584

584585
public static DialogClosingEventHandler GetDialogClosingAttached(DependencyObject element)
585-
{
586-
return (DialogClosingEventHandler)element.GetValue(DialogClosingAttachedProperty);
587-
}
586+
=> (DialogClosingEventHandler)element.GetValue(DialogClosingAttachedProperty);
588587

589588
public static readonly DependencyProperty DialogClosingCallbackProperty = DependencyProperty.Register(
590589
nameof(DialogClosingCallback), typeof(DialogClosingEventHandler), typeof(DialogHost), new PropertyMetadata(default(DialogClosingEventHandler)));
@@ -599,9 +598,7 @@ public DialogClosingEventHandler? DialogClosingCallback
599598
}
600599

601600
protected void OnDialogClosing(DialogClosingEventArgs eventArgs)
602-
{
603-
RaiseEvent(eventArgs);
604-
}
601+
=> RaiseEvent(eventArgs);
605602

606603
#endregion
607604

@@ -616,16 +613,16 @@ internal void AssertTargetableContent()
616613
internal void InternalClose(object? parameter)
617614
{
618615
var currentSession = CurrentSession ?? throw new InvalidOperationException($"{nameof(DialogHost)} does not have a current session");
619-
var dialogClosingEventArgs = new DialogClosingEventArgs(currentSession, DialogClosingEvent);
620616

621617
currentSession.CloseParameter = parameter;
622618
currentSession.IsEnded = true;
623619

624620
//multiple ways of calling back that the dialog is closing:
625621
// * routed event
626622
// * the attached property (which should be applied to the button which opened the dialog
627-
// * straight forward dependency property
623+
// * straight forward IsOpen dependency property
628624
// * handler provided to the async show method
625+
var dialogClosingEventArgs = new DialogClosingEventArgs(currentSession, DialogClosingEvent);
629626
OnDialogClosing(dialogClosingEventArgs);
630627
_attachedDialogClosingEventHandler?.Invoke(this, dialogClosingEventArgs);
631628
DialogClosingCallback?.Invoke(this, dialogClosingEventArgs);
@@ -690,21 +687,14 @@ private void OpenDialogHandler(object sender, ExecutedRoutedEventArgs executedRo
690687

691688
if (_popupContentControl != null)
692689
{
693-
switch (OpenDialogCommandDataContextSource)
690+
_popupContentControl.DataContext = OpenDialogCommandDataContextSource switch
694691
{
695-
case DialogHostOpenDialogCommandDataContextSource.SenderElement:
696-
_popupContentControl.DataContext =
697-
(executedRoutedEventArgs.OriginalSource as FrameworkElement)?.DataContext;
698-
break;
699-
case DialogHostOpenDialogCommandDataContextSource.DialogHostInstance:
700-
_popupContentControl.DataContext = DataContext;
701-
break;
702-
case DialogHostOpenDialogCommandDataContextSource.None:
703-
_popupContentControl.DataContext = null;
704-
break;
705-
default:
706-
throw new ArgumentOutOfRangeException();
707-
}
692+
DialogHostOpenDialogCommandDataContextSource.SenderElement
693+
=> (executedRoutedEventArgs.OriginalSource as FrameworkElement)?.DataContext,
694+
DialogHostOpenDialogCommandDataContextSource.DialogHostInstance => DataContext,
695+
DialogHostOpenDialogCommandDataContextSource.None => null,
696+
_ => throw new ArgumentOutOfRangeException(),
697+
};
708698
}
709699

710700
DialogContent = executedRoutedEventArgs.Parameter;
@@ -716,9 +706,7 @@ private void OpenDialogHandler(object sender, ExecutedRoutedEventArgs executedRo
716706
}
717707

718708
private void CloseDialogCanExecute(object sender, CanExecuteRoutedEventArgs canExecuteRoutedEventArgs)
719-
{
720-
canExecuteRoutedEventArgs.CanExecute = CurrentSession != null;
721-
}
709+
=> canExecuteRoutedEventArgs.CanExecute = CurrentSession != null;
722710

723711
private void CloseDialogHandler(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs)
724712
{
@@ -730,19 +718,13 @@ private void CloseDialogHandler(object sender, ExecutedRoutedEventArgs executedR
730718
}
731719

732720
private string GetStateName()
733-
{
734-
return IsOpen ? OpenStateName : ClosedStateName;
735-
}
721+
=> IsOpen ? OpenStateName : ClosedStateName;
736722

737723
private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
738-
{
739-
LoadedInstances.Remove(this);
740-
}
724+
=> LoadedInstances.Remove(this);
741725

742726
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
743-
{
744-
LoadedInstances.Add(this);
745-
}
727+
=> LoadedInstances.Add(this);
746728

747729
}
748730
}

MaterialDesignThemes.Wpf/DialogSession.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ public class DialogSession
1111
private readonly DialogHost _owner;
1212

1313
internal DialogSession(DialogHost owner)
14-
{
15-
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
16-
}
14+
=> _owner = owner ?? throw new ArgumentNullException(nameof(owner));
1715

1816
/// <summary>
1917
/// Indicates if the dialog session has ended. Once ended no further method calls will be permitted.

0 commit comments

Comments
 (0)