从 BackgroundWorker 运行时 CrystalReportViewer.RefreshReport 挂起
Posted
技术标签:
【中文标题】从 BackgroundWorker 运行时 CrystalReportViewer.RefreshReport 挂起【英文标题】:CrystalReportViewer.RefreshReport hangs when running from BackgroundWorker 【发布时间】:2021-12-29 15:58:09 【问题描述】:我正在尝试通过在准备/加载 Crystal Report 时添加加载屏幕来“增强”我的报告代码。在我开始尝试添加加载屏幕之前,我的所有报告都会正常显示,但光标的变化并不足以表明应用程序仍在拉动报告——其中一些可能需要一段时间 - 所以我想提供一个更“明显”的视觉提示。
为了实现这一点,我将报告创建方法调用放入加载屏幕本身中存在的BackgroundWorker
中(我还没有开始学习如何很好地使用Async/Await
但使用它感觉很舒服)。加载屏幕正确出现,一切似乎都按预期工作,直到它实际尝试在屏幕上显示报告。此时,“请稍候,文档正在处理。”框出现(在用于显示报告的表单中的 CrystalReportViewer
控件中),但它只是坐在那里,甚至不旋转。最终,我的 IDE 抛出关于接收 ContextSwitchDeadlock
的错误,我几乎只需要取消执行。
这是我的 dlgReportLoading
“启动画面”,带有一个包含动画 GIF 的 PictureBox
控件:
Imports System.Windows.Forms
Public Class dlgReportLoading
Private DisplayReport As Common.CRReport
Private WithEvents LoadReportWorker As System.ComponentModel.BackgroundWorker
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
End Sub
Private Sub dlgReportLoading_Load(sender As Object, e As EventArgs) Handles Me.Load
Me.Cursor = Cursors.WaitCursor
Me.TopMost = True
Me.TopMost = False
LoadReportWorker = New System.ComponentModel.BackgroundWorker
LoadReportWorker.RunWorkerAsync()
End Sub
Private Sub dlgReportLoading_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
Me.Cursor = Cursors.Default
End Sub
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
If Not DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None Then
Select Case DisplayReport.ReportOption
Case Common.CRReport.GenerateReportOption.DisplayOnScreen
'-- This is the method I'm currently testing
DisplayReport.ShowReport()
Case Common.CRReport.GenerateReportOption.SendToPrinter
DisplayReport.PrintReport()
Case Common.CRReport.GenerateReportOption.ExportToFile
DisplayReport.ExportReport()
End Select
End If
DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None
'--
'-- This code was in use before trying to generate the reports in the background
'If Not DisplayReport.CrystalReport Is Nothing Then
' DisplayReport.CrystalReport.Dispose()
'End If
'--
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
End Class
如上面代码中所述,我目前正在测试此处定义的ShowReport()
方法:
Protected Friend Sub ShowReport()
Dim ReportViewer As frmReportPreview
Me.PrepareReport()
ReportViewer = New frmReportPreview(Me)
With ReportViewer
.WindowState = FormWindowState.Maximized
.Show()
End With
End Sub
frmReportPreview
是这样的:
Imports System.ComponentModel
Public Class frmReportPreview
Private DisplayReport As Common.CRReport
Private ReportToDisplay As CrystalDecisions.CrystalReports.Engine.ReportDocument
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
PrepareReportForDisplay()
Me.rptViewer.ReportSource = Nothing
Me.rptViewer.ReportSource = ReportToDisplay
' SET ZOOM LEVEL FOR DISPLAY:
' 1 = Page Width
' 2 = Whole Page
' 25-100 = zoom %
Me.rptViewer.Zoom(1)
Me.rptViewer.Show()
End Sub
Private Sub frmReportPreview_Shown(sender As Object, e As EventArgs) Handles Me.Shown
'-- HANGS HERE
Me.rptViewer.RefreshReport()
End Sub
Private Sub frmReportPreview_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
ReportToDisplay.Dispose()
Me.rptViewer.ReportSource = Nothing
End Sub
'...CODE FOR PREPARING THE REPORT TO BE DISPLAYED
End Class
dlgReportLoading
表单正确弹出并播放动画,直到 frmReportPreview
在其前面弹出(它不会关闭)。带有通常是动画旋转圆圈的小框显示正在加载报告数据,但几乎立即冻结在原地。
在调用ShowReport()
方法后,我的dlgReportLoading
表单的LoadReport_DoWork()
方法中有一个断点,但它永远不会到达那个点。我在该表单的 LoadReport_Complete()
方法中也有一个,它从来没有命中过,而且该对话框也从未真正关闭。
我在frmReportPreview_Shown
方法的末尾放置了另一个断点,就在Me.rptViewer.RefreshReport()
调用之后,但它也从来没有遇到过,所以很明显这是卡住的地方,但只有 当通过BackgroundWorker
生成报告时。如果我只是调用ShowReport()
方法而不通过“启动画面”和BackgroundWorker
发送它,一切都会正常生成并显示。
我尝试将RefreshReport()
方法放入其自己的 BackgroundWorker
中,但行为没有改变。我尝试使用ShowDialog()
而不是仅Show()
以模态方式显示frmReportPreview
对象。这些似乎都没有帮助解决这个问题。
我感觉某处有些东西被处理得太早了,但我不知道那会是什么。如果需要,我可以从frmReportPreview
提供报告准备代码的其余部分,但据我所知,所有 似乎 都在正常工作。我不反对尝试其他方法来实现我的目标,即在所有报告准备过程中向用户显示加载屏幕 - 例如,Async/Await
或其他多线程方法 - 所以 any 欢迎提出建议。如果需要任何其他说明,请告诉我。
环境
Microsoft Windows 10 Pro 21H1(操作系统内部版本 19043.1348) Microsoft Visual Studio 社区 2017 (v15.9.38) Crystal Reports for .NET Framework v13.0.3500.0(运行时版本 2.0.50727)
编辑:我忘了提到这整个混乱是从我的CRReport
类中的GenerateReport()
方法调用的,定义为:
Public Sub GenerateReport(ByVal ReportGeneration As GenerateReportOption)
Me.ReportOption = ReportGeneration
If Me.ReportOption = GenerateReportOption.None Then
'...CODE FOR REQUESTING A GENERATION OPTION FROM THE USER
End If
Dim ReportLoadingScreen As New dlgReportLoading(Me)
ReportLoadingScreen.ShowDialog()
End Sub
这反过来又是从我的主窗体中调用的,如下所示:
Private Sub PrintMyXMLReport(ByVal XMLFile As IO.FileInfo)
Dim MyXMLReport As New IO.FileInfo("\\SERVER\Applications\Reports\MyXMLReport.rpt")
Dim Report As New Common.CRReport(MyXMLReport, XMLFile)
Report.GenerateReport(Common.CRReport.GenerateReportOption.DisplayOnScreen)
End Sub
【问题讨论】:
TBH 有很多代码要看,所以乍一看我并没有真正了解你在做什么。我看到了样本,看起来它应该可以工作。但是我不喜欢在加载屏幕中定义后台工作人员的想法,这将是我对该示例的批评。为了分离 UI 和业务逻辑,调用者不应该知道 UI 存在。我不确定这种跨 UI 和非 UI 线程调用的线程顺序是否与它有关,但我个人不会根据该示例调用它。 好的,现在我实际上已经查看了您的代码 :) 并且您知道BackgroundWorker
是在非 UI 线程上运行的,至少 LoadReport_DoWork 是,所以您不应该做 UI操作。但是在ShowReport
中,您创建了一个新表单并显示它。您应该在 UI 上调用该调用。为此,您需要对 UI 上的表单或控件进行某种引用。但是,我认为最好显示来自 LoadReport_Complete
方法的报告,因为它会被调用回调用者,如果我没记错的话,这是对话框并在你的情况下在 UI 上运行。
我觉得你应该把Protected Friend Sub ShowReport() Me.PrepareReport() ReportViewer.Show()
拆分成两个方法:PrepareReport
和ShowReport
,其中的内容应该是不言而喻的,然后在LoadReport_DoWork
调用prepare report,在LoadReport_Complete
。在重新开始之前,请先尝试一下。我很想看看这是否有效。
天啊!你是一个可怕的救生员!!! (而不是糖果)PrepareReport()
方法已经是独特的(但私有的),所以我将其更改为 Protected Friend
方法,将 that 放入 @987654364 @handler,将其从ShowReport
方法中注释掉(以避免重复),然后将整个Select Case
块移动到RunWorkerCompleted
处理程序中。现在一切似乎都按预期工作了!太感谢了。将该建议作为答案发布,我很乐意接受! (我也可以提供最终的代码实现):D
太棒了。我个人只很少使用 BackgroundWorker,并且对 System.Threading.Thread
和 Async / Await
有更多经验。但是 UI 和非 UI 之间的跨线程原则仍然适用,了解每个 BackgroundWorker
事件的运行方式是关键。
【参考方案1】:
您应该将繁重的工作和 UI 操作分成不同的方法,以便将它们放入适当的 BackgroundWorker 事件中:
Protected Friend Sub PrepareReport()
' perform long-running background work
End Sub
Protected Friend Sub ShowReport()
Dim ReportViewer = New frmReportPreview(Me) With .WindowState = FormWindowState.Maximized
ReportViewer.Show()
End Sub
Private DisplayReport As Common.CRReport
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
DisplayReport.PrepareReport()
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
DisplayReport.ShowReport()
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
因为LoadReport_DoWork
实际上运行在一个新的非UI线程上,而LoadReport_Complete
运行在调用者线程上,也就是一个UI线程。只有在那里你才能与 UI 交互并显示表单等。
【讨论】:
以上是关于从 BackgroundWorker 运行时 CrystalReportViewer.RefreshReport 挂起的主要内容,如果未能解决你的问题,请参考以下文章
为啥 BackgroundWorker 的 Progress 事件运行在不同的线程上?