Skip to content

Commit ca2ebe0

Browse files
authored
Let the viewer close when Windows is shutting down (#778)
OnFormClosing cancelled every close, whatever the CloseReason. For a close the user asked for that is right: whether closing means hide or exit is ViewerProgram's rule and it needs a tray check to decide, so the form defers and CloseForReal brings the answer back. For the close Windows sends when the session is ending it is not. WinForms answers WM_QUERYENDSESSION with !e.Cancel, so a viewer that was open at shutdown told Windows it was preventing one, and the user got the "DiffEngineViewer is preventing shutdown" screen. With a tray running it was worse than a prompt: the loop's answer to a close request is to hide the window, so the process stayed up and went on blocking until the user chose "Shut down anyway". Do not cancel when the reason is WindowsShutDown or TaskManagerClosing. Both mean the process is going away regardless, so refusing buys nothing. Letting the close through is safe: the loop already watches for a disposed form and returns, running the same shutdown it would have run anyway.
1 parent d7f8a27 commit ca2ebe0

3 files changed

Lines changed: 71 additions & 4 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
global using System.Buffers.Binary;
2-
global using System.IO.Compression;
2+
global using System.IO.Compression;global using System.Reflection;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// <summary>
2+
/// Who gets to refuse a close.
3+
/// <para>
4+
/// The form cancels a user close because whether closing means hide or exit is ViewerProgram's
5+
/// rule, not the form's. It used to cancel every close, including the one Windows sends when the
6+
/// session is ending — and WinForms answers WM_QUERYENDSESSION with !e.Cancel, so the viewer
7+
/// reported itself as preventing shutdown.
8+
/// </para>
9+
/// <para>
10+
/// Driven through OnFormClosing by reflection, because CloseReason is set by the message that
11+
/// started the close and there is no way to ask a form to close as though Windows had.
12+
/// </para>
13+
/// </summary>
14+
[NotInParallel]
15+
[TUnit.Core.Executors.STAThreadExecutor]
16+
public class ViewerFormClosingTests
17+
{
18+
[Test]
19+
[Arguments(CloseReason.UserClosing, true)]
20+
[Arguments(CloseReason.None, true)]
21+
[Arguments(CloseReason.WindowsShutDown, false)]
22+
[Arguments(CloseReason.TaskManagerClosing, false)]
23+
public async Task Cancels(CloseReason reason, bool expected)
24+
{
25+
using var form = new ViewerForm("title", 800, 600);
26+
var args = new FormClosingEventArgs(reason, false);
27+
28+
Raise(form, args);
29+
30+
await Assert.That(args.Cancel).IsEqualTo(expected);
31+
}
32+
33+
/// <summary>
34+
/// And CloseForReal still wins, whatever the reason, since that is the loop answering its own
35+
/// question.
36+
/// </summary>
37+
[Test]
38+
public async Task CloseForRealIsNeverCancelled()
39+
{
40+
using var form = new ViewerForm("title", 800, 600);
41+
form.CloseForReal();
42+
var args = new FormClosingEventArgs(CloseReason.UserClosing, false);
43+
44+
Raise(form, args);
45+
46+
await Assert.That(args.Cancel).IsFalse();
47+
}
48+
49+
static void Raise(ViewerForm form, FormClosingEventArgs args) =>
50+
typeof(ViewerForm)
51+
.GetMethod("OnFormClosing", BindingFlags.Instance | BindingFlags.NonPublic)!
52+
.Invoke(form, [args]);
53+
}

src/DiffEngineViewer.Windows/ViewerForm.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,16 @@ public void CloseForReal()
312312

313313
protected override void OnFormClosing(FormClosingEventArgs e)
314314
{
315-
// Always cancelled, because whether closing means hide or exit is ViewerProgram's rule and
316-
// it needs a tray check to decide. CloseForReal is how the answer comes back.
317-
if (!closingForReal)
315+
// Cancelled for a close the user asked for, because whether that means hide or exit is
316+
// ViewerProgram's rule and it needs a tray check to decide. CloseForReal is how the answer
317+
// comes back.
318+
//
319+
// Never for a close the session is ending: WinForms answers WM_QUERYENDSESSION with
320+
// !e.Cancel, so refusing made Windows report the viewer as preventing shutdown, and with a
321+
// tray running the loop only hid the window - leaving the process blocking until the user
322+
// chose "Shut down anyway". Letting it through is safe because the loop watches for a
323+
// disposed form and returns, which runs the same shutdown it would have run anyway.
324+
if (!closingForReal && !EndsTheSession(e.CloseReason))
318325
{
319326
closeRequested = true;
320327
e.Cancel = true;
@@ -323,6 +330,13 @@ protected override void OnFormClosing(FormClosingEventArgs e)
323330
base.OnFormClosing(e);
324331
}
325332

333+
/// <summary>
334+
/// The process is going away whatever this form says. Task Manager's End Task is here with
335+
/// shutdown because refusing it buys the same nothing: the user has already decided.
336+
/// </summary>
337+
internal static bool EndsTheSession(CloseReason reason) =>
338+
reason is CloseReason.WindowsShutDown or CloseReason.TaskManagerClosing;
339+
326340
/// <summary>
327341
/// ProcessCmdKey rather than OnKeyDown, because Tab and Escape are consumed by focus
328342
/// navigation and the default button before a key handler would ever see them.

0 commit comments

Comments
 (0)