从 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()拆分成两个方法:PrepareReportShowReport,其中的内容应该是不言而喻的,然后在LoadReport_DoWork调用prepare report,在LoadReport_Complete。在重新开始之前,请先尝试一下。我很想看看这是否有效。 天啊!你是一个可怕的救生员!!! (而不是糖果PrepareReport() 方法已经是独特的(但私有的),所以我将其更改为 Protected Friend 方法,将 that 放入 @987654364 @handler,将其从ShowReport 方法中注释掉(以避免重复),然后将整个Select Case 块移动到RunWorkerCompleted 处理程序中。现在一切似乎都按预期工作了!太感谢了。将该建议作为答案发布,我很乐意接受! (我也可以提供最终的代码实现):D 太棒了。我个人只很少使用 BackgroundWorker,并且对 System.Threading.ThreadAsync / 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 事件运行在不同的线程上?

backgroundworker控件

C#timer backgroundworker用户控件

如何正确实现带有 ProgressBar 更新的 BackgroundWorker?

BackgroundWorker用法示例

C#中的BackgroundWorker控件