How to reset the close reason when close is cancelled

.net-4.0, vb.net, winforms

Solution

I would probably call that a bug.

As you mentioned, the CloseReason property is marked internal (or Friend in VB.Net terms) so one work-around to the problem is using Reflection to reset that value yourself:

Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
  If Not exitPending Then
    e.Cancel = True
    tbOutput.AppendText(String.Format("DialogResult={0}, CloseReason={1}{2}", _
                        Me.DialogResult.ToString(), e.CloseReason.ToString(), _
                        Environment.NewLine))
    Dim pi As PropertyInfo
    pi = Me.GetType.GetProperty("CloseReason", _
                                BindingFlags.Instance Or BindingFlags.NonPublic)
    pi.SetValue(Me, CloseReason.None, Nothing)
  End If
  MyBase.OnFormClosing(e)
End Sub

No guarantee that this code would work on future versions of WinForms, but I'm guessing it's a safe bet these days. :-)

Problem

Question Is it possible to reset the CloseReason provided by the FormClosingEventArgs in the FormClosing event of a modal dialog? Symptoms Setting the `DialogResult` of a modal dialog can result in an "incorrect" `CloseReason` if the close event have previously been cancelled. Details (The following code is just sample code to highlight the inconvenience) Imagine I have a form with two buttons, OK and Cancel, displayed as a modal dialog. ``` Me.btnOk = New Button With {.DialogResult = Windows.Forms.DialogResult.OK} Me.btnCancel = New Button With {.DialogResult = Windows.Forms.DialogResult.Cancel} Me.AcceptButton = Me.btnOk Me.CancelButton = Me.btnCancel ``` Any attempts to close the form will be cancelled. If I click each button (including the `[X]` - close form button) in the following order, the close reasons will be as following: Case 1 - `btnOk`::::::::::: None - `btnCancel`::: None - `X`::::::::::::::::::: UserClosing Now, if I repeat the steps you'll see that the `UserClosing` reason will persist: - `btnOk`::::::::::: UserClosing - `btnCancel`::: UserClosing - `X`::::::::::::::::::: UserClosing Case 2 - `X`::::::::::::::::::: UserClosing - `btnCancel`::: UserClosing - `btnOk`::::::::::: UserClosing Same here. Once you click the `X` button the close reason will always return `UserClosing`. Sample application ``` Public Class Form1 Public Sub New() Me.InitializeComponent() Me.Text = "Test" Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog Me.MinimizeBox = False Me.MaximizeBox = False Me.ClientSize = New Size(75, 25) Me.StartPosition = FormStartPosition.CenterScreen Me.btnOpenDialog = New Button() With {.TabIndex = 0, .Dock = DockStyle.Fill, .Text = "Open dialog"} Me.Controls.Add(Me.btnOpenDialog) End Sub Private Sub HandleOpenDialog(sender As Object, e As EventArgs) Handles btnOpenDialog.Click Using instance As New CustomDialog() instance.ShowDialog() End Using End Sub Private WithEvents btnOpenDialog As Button Private Class CustomDialog Inherits Form Public Sub New() Me.Text = "Custom dialog" Me.ClientSize = New Size(400, 200) Me.StartPosition = FormStartPosition.CenterParent Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog Me.MinimizeBox = False Me.MaximizeBox = False Me.tbOutput = New RichTextBox() With {.TabIndex = 0, .Bounds = New Rectangle(0, 0, 400, 155), .ReadOnly = True, .ScrollBars = RichTextBoxScrollBars.ForcedBoth, .WordWrap = True} Me.btnExit = New Button With {.TabIndex = 3, .Text = "Exit", .Bounds = New Rectangle(10, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Left)} Me.btnOk = New Button With {.TabIndex = 1, .Text = "OK", .Bounds = New Rectangle(237, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.OK} Me.btnCancel = New Button With {.TabIndex = 2, .Text = "Cancel", .Bounds = New Rectangle(315, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.Cancel} Me.Controls.AddRange({Me.tbOutput, Me.btnExit, Me.btnOk, Me.btnCancel}) Me.AcceptButton = Me.btnOk Me.CancelButton = Me.btnCancel End Sub Private Sub HandleExitDialog(sender As Object, e As EventArgs) Handles btnExit.Click Me.exitPending = True Me.Close() End Sub Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs) If (Not Me.exitPending) Then e.Cancel = True Me.tbOutput.Text += (String.Format("DialogResult={0}, CloseReason={1}{2}", Me.DialogResult.ToString(), e.CloseReason.ToString(), Environment.NewLine)) Me.DialogResult = Windows.Forms.DialogResult.None End If MyBase.OnFormClosing(e) End Sub Private exitPending As Boolean Private WithEvents btnExit As Button Private WithEvents btnCancel As Button Private WithEvents btnOk As Button Private WithEvents tbOutput As RichTextBox End Class End Class ``` Update I was of the impression that if either the `Form.AcceptButton` or `Form.CancelButton` (IButtonControl) was clicked the close reason would be set to `UserClosing`, but this is not the case. In the following code you'll see that all it do is setting the `DialogResult` of the owning form to that of its own `DialogResult`. ``` Protected Overrides Sub OnClick(ByVal e As EventArgs) Dim form As Form = MyBase.FindFormInternal If (Not form Is Nothing) Then form.DialogResult = Me.DialogResult End If MyBase.AccessibilityNotifyClients(AccessibleEvents.StateChange, -1) MyBase.AccessibilityNotifyClients(AccessibleEvents.NameChange, -1) MyBase.OnClick(e) End Sub ``` The `Control` class do have a property named `CloseReason` but it's defined as `Friend`, thus not accessible. I also thought that setting the forms `DialogResult` would result in a `WM` message being sent, but all it does is setting a private field. So I delved into reflector and followed the stack. The following image is a highly simplified illustration. This is how the `CheckCloseDialog` method looks like: ``` Friend Function CheckCloseDialog(ByVal closingOnly As Boolean) As Boolean If ((Me.dialogResult = DialogResult.None) AndAlso MyBase.Visible) Then Return False End If Try Dim e As New FormClosingEventArgs(Me.closeReason, False) If Not Me.CalledClosing Then Me.OnClosing(e) Me.OnFormClosing(e) If e.Cancel Then Me.dialogResult = DialogResult.None Else Me.CalledClosing = True End If End If If (Not closingOnly AndAlso (Me.dialogResult <> DialogResult.None)) Then Dim args2 As New FormClosedEventArgs(Me.closeReason) Me.OnClosed(args2) Me.OnFormClosed(args2) Me.CalledClosing = False End If Catch exception As Exception Me.dialogResult = DialogResult.None If NativeWindow.WndProcShouldBeDebuggable Then Throw End If Application.OnThreadException(exception) End Try If (Me.dialogResult = DialogResult.None) Then Return Not MyBase.Visible End If Return True End Function ``` As you can see the modal message loop checks the `DialogResult` in every cycle and if the conditions are met it will use the stored `CloseReason` (as observed) when creating the `FormClosingEventArgs`. Summary Yes, I know that the `IButtonControl` interface have a `PerformClick` method which you can call programmatically, but still, IMO this smells like a bug. If clicking a button is not a result of a user action then what is?

Original source