Bjø*_*sjå 7 vb.net .net-4.0 winforms
题
是否可以在模式对话框的FormClosing事件中重置FormClosingEventArgs提供的CloseReason?
症状
如果先前已取消关闭事件,则设置DialogResult模态对话框可能会导致"不正确" CloseReason.
细节
(以下代码只是示例代码,以突出不便)
想象一下,我有一个带有两个按钮的表单,OK和Cancel,显示为模态对话框.
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
Run Code Online (Sandbox Code Playgroud)
任何关闭表单的尝试都将被取消.
如果按[X]以下顺序单击每个按钮(包括- 关闭表单按钮),则关闭原因如下:
情况1
btnOk::::::::::: 无btnCancel::: 没有X::::::::::::::::::: UserClosing现在,如果我重复这些步骤,你会发现UserClosing原因会持续存在:
btnOk::::::::::: UserClosingbtnCancel::: UserClosingX::::::::::::::::::: UserClosing案例2
X::::::::::::::::::: UserClosingbtnCancel::: UserClosingbtnOk::::::::::: UserClosing同样在这里.单击X按钮后,关闭原因将始终返回UserClosing.
样品申请
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
Run Code Online (Sandbox Code Playgroud)
我的印象是,如果单击Form.AcceptButton或Form.CancelButton (IButtonControl)被单击,则关闭原因将被设置为UserClosing,但事实并非如此.在下面的代码中,您将看到它所做的只是将DialogResult拥有的表单设置为它自己的表单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
Run Code Online (Sandbox Code Playgroud)
该Control班也有一个命名的属性CloseReason,但它的定义为Friend,从而无法访问.
我还认为设置表单DialogResult会导致WM发送消息,但它只是设置一个私有字段.
所以我钻研了反射器并跟着堆栈.下图是高度简化的插图.

这是CheckCloseDialog方法的样子:
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
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,模态消息循环会检查DialogResult每个循环,如果满足条件,它将在创建时使用存储的 CloseReason(如所观察到的)FormClosingEventArgs.
摘要
是的,我知道IButtonControl界面有一个PerformClick你可以用编程方式调用的方法,但是,IMO这个闻起来像一个bug.如果单击按钮不是用户操作的结果,那么是什么?
理解为什么这样做会非常重要,当你过分依赖CloseReason时,你可能会遇到麻烦.这不是一个错误,由于Windows的设计方式,这是一个限制.一个核心问题是WM_CLOSE消息的制定方式,它是设置列车运行的方式,首先触发FormClosing事件.
这个消息可以被发送大量的原因,你所熟悉的常见的.但这不是它结束的地方,其他程序也可以发送该消息.您可以告诉我链接到的MSDN Library文章中的"缺陷",消息缺少编码消息意图的WPARAM值.因此,程序没有任何方法可以向您提供合理的CloseReason.Winforms被迫猜测一个原因.这当然是完全不完美的猜测.
这不是它结束的地方,DialogResult属性也是一个问题.当任何代码分配该属性时,它将强制关闭对话框.但同样的问题,这种代码没有任何方法来表明赋值的意图.所以它没有,它在内部Form.CloseReason属性中保留它之前的任何值,默认情况下为None.
这是在.NET 1.0中"正确"实现的,只有Closing事件并没有给出任何理由.但这也没有那么好用,使用它的应用程序长期阻止Windows关闭.他们只是不知道显示一个消息框是不合适的.添加了.NET 2.0 FormClosing事件作为解决方法.但它需要与不完美的猜测一起工作.
对CloseReason值进行评级很重要,有些非常准确,有些只是猜测:
是的,当FormClosing事件处理程序取消时,Winforms没有将CloseReason设置回None,这可能是一个错误.但这并不是真正重要的那种错误.因为无论如何你都不能区别对待UserClosing和None.
我可能会称之为错误。
正如您所提到的,CloseReason 属性被标记为内部(或 VB.Net 术语中的 Friend),因此解决该问题的一种方法是使用 Reflection 自行重置该值:
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
Run Code Online (Sandbox Code Playgroud)
不能保证此代码适用于 WinForms 的未来版本,但我猜现在这是一个安全的选择。:-)
| 归档时间: |
|
| 查看次数: |
1868 次 |
| 最近记录: |