C#:在 ReportViewer 中单击刷新按钮几次后应用程序崩溃

C# : Application crashes after clicking Refresh button in ReportViewer a few times

简介

这是我的 Winforms 应用程序中 ReportViewer 的一部分,该应用程序的目标是 .NET Framework 4.6.1。

确定按钮调用 btnOk_Click 事件,而刷新按钮(圆圈中的双绿色箭头)调用 ReportViewer 事件,该事件本身调用带有空参数的 btnOK_Click 事件。下面的代码说明了这一点

代码

private void btnOk_Click(object sender, EventArgs e)
    {
        try
        {
            ...
            //Code adding datasources to rpvCustomReport
            ...
            this.rpvCustomReport.RefreshReport(); //Causing the error.
            //NOTE: this.rpvCustomReport is instantiated in .Design.cs as
            //new Microsoft.Reporting.WinForms.ReportViewer();
            ...
        }
        catch (Exception ex)
        {
            HandleException(ex); //Custom function to handle exception
        }
    }

private void rpvCustomReport_ReportRefresh(object sender, CancelEventArgs e)
    {
        this.btnOk_Click(null, null); //sender object and event variables are not used in this function, hence, it is set to null
    }

问题

我的应用程序在单击 ReportViewer 中的刷新按钮几次后崩溃。

这是我在 事件查看器 > Windows 日志 > 应用程序

中找到的内容

消息:AsyncLocal 通知回调中未处理异常。我尝试使用谷歌搜索错误,但结果很短。

线索

  1. 调试模式(Visual Studio 2015)下不会出现此问题
  2. 当我快速单击确定按钮时,不会出现此问题。
  3. 当我在 [=12= 中添加 redundant/test 代码(例如 MessageBox.Show()after this.rpvCustomReport.RefreshReport(); 行时,不会发生此问题] 事件。但是当我在 before 该行添加它们时,问题发生了。这就是我得出的结论 this.rpvCustomReport.RefreshReport(); 导致问题的原因。

问题

  1. 为什么会出现这个问题?
  2. 我应该执行哪些步骤才能在将来调试此类问题?

解决方法

为了解决这个问题,我必须在调用 btnOk_Click 之前取消活动。

private void rpvCustomReport_ReportRefresh(object sender, CancelEventArgs e)
{
    e.Cancel = true; //Cancel the default event.
    this.btnOk_Click(null, null); //sender object and event variables are not used in this function, hence, it is set to null
}

我还是不明白为什么我需要取消默认行为。这似乎不是一个很好的解决方案。

能不能换码

this.btnOk_Click(null, null);

this.btnOk_Click(sender, e);

如果没有解决问题请告诉我

你应该可以使用

btnOk.PerformClick();

另外,最好使用 try - catch 块并记录运行时发生的错误,您将无法始终调试应用程序的源代码。

可以不带参数调用按钮点击函数

btnOk.Click();或 btnOk.PerformClick();

或者修改如下代码试试

private void doSomeThing() {
try
    {
        ...
        //Code adding datasources to rpvCustomReport
        ...
        this.rpvCustomReport.RefreshReport(); //Causing the error.
        //NOTE: this.rpvCustomReport is instantiated in .Design.cs as
        //new Microsoft.Reporting.WinForms.ReportViewer();
        ...
    }
    catch (Exception ex)
    {
        HandleException(ex); //Custom function to handle exception
    }
}

private void btnOk_Click(object sender, EventArgs e)
{
    this.doSomeThing();
}

private void rpvCustomReport_ReportRefresh(object sender, CancelEventArgs e)
{
    this.doSomeThing();
}

我也在 MSDN 上发布了对您问题的回复。

这与 ReportViewer 的内部异步呈现有关,当您导致它在当前操作的中间取消并重新开始时。我 运行 通过加载我的报告然后立即将显示模式设置为打印布局来进入它。通过实验,可以通过使用以下代码向表单添加一个按钮然后重复单击它来将此问题减少为可重复的故障(警告是,如您所述,在调试器中 运行 时不会出现此问题):

form.ReportViewer.SetDisplayMode(DisplayMode.PrintLayout);
form.ReportViewer.SetDisplayMode(DisplayMode.Normal);

在您的例子中,单击 ReportViewer 的刷新按钮会导致报表触发其内部刷新例程。该代码如下所示(使用 JetBrains dotPeek 提取,尽管 Microsoft 现在已将其开源,因此您可以在 MS 代码参考站点上找到):

private void OnRefresh(object sender, EventArgs e)
{
  try
  {
    CancelEventArgs e1 = new CancelEventArgs();
    if (this.ReportRefresh != null)
      this.ReportRefresh((object) this, e1);
    if (e1.Cancel)
      return;
    int targetPage = 1;
    PostRenderArgs postRenderArgs = (PostRenderArgs) null;
    if (sender == this.m_autoRefreshTimer)
    {
      targetPage = this.CurrentPage;
      postRenderArgs = new PostRenderArgs(true, false, this.winRSviewer.ReportPanelAutoScrollPosition);
    }
    this.RefreshReport(targetPage, postRenderArgs);
  }
  catch (Exception ex)
  {
    this.UpdateUIState(ex);
  }
}

请注意,已引发 ReportRefresh 事件,如果不取消此事件,ReportViewer 将继续处理并重新呈现报表。您在事件处理程序中的代码还告诉 ReportViewer 刷新,这基本上设置了与我的代码一样的鞭打 ReportViewer 的相同问题。

我原本打算通过在 MS Connect 上提交一份官方错误报告的想法进一步隔离这个问题,但我已经尽可能深入兔子洞了。我们从调用堆栈中知道的是一个线程正在切换执行上下文:

Description: The application requested process termination through System.Environment.FailFast(string message).
Message: An exception was not handled in an AsyncLocal<T> notification callback.
Stack:
   at System.Environment.FailFast(System.String, System.Exception)
   at System.Threading.ExecutionContext.OnAsyncLocalContextChanged(System.Threading.ExecutionContext, System.Threading.ExecutionContext)
   at System.Threading.ExecutionContext.SetExecutionContext(System.Threading.ExecutionContext, Boolean)
   at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
   at System.Threading.ThreadHelper.ThreadStart(System.Object)

当 OnAsyncLocalContextChanged 触发时,它会尝试处理更改通知的回调:

[SecurityCritical]
[HandleProcessCorruptedStateExceptions]
internal static void OnAsyncLocalContextChanged(ExecutionContext previous, ExecutionContext current)
{
    List<IAsyncLocal> previousLocalChangeNotifications = (previous == null) ? null : previous._localChangeNotifications;
    if (previousLocalChangeNotifications != null)
    {
        foreach (IAsyncLocal local in previousLocalChangeNotifications)
        {
            object previousValue = null;
            if (previous != null && previous._localValues != null)
                previous._localValues.TryGetValue(local, out previousValue);

            object currentValue = null;
            if (current != null && current._localValues != null)
                current._localValues.TryGetValue(local, out currentValue);

            if (previousValue != currentValue)
                local.OnValueChanged(previousValue, currentValue, true);
        }
    }

    List<IAsyncLocal> currentLocalChangeNotifications = (current == null) ? null : current._localChangeNotifications;
    if (currentLocalChangeNotifications != null && currentLocalChangeNotifications != previousLocalChangeNotifications)
    {
        try
        {
            foreach (IAsyncLocal local in currentLocalChangeNotifications)
            {
                // If the local has a value in the previous context, we already fired the event for that local
                // in the code above.
                object previousValue = null;
                if (previous == null ||
                    previous._localValues == null ||
                    !previous._localValues.TryGetValue(local, out previousValue))
                {
                    object currentValue = null;
                    if (current != null && current._localValues != null)
                        current._localValues.TryGetValue(local, out currentValue);

                    if (previousValue != currentValue)
                        local.OnValueChanged(previousValue, currentValue, true);
                }
            }
        }
        catch (Exception ex)
        {
            Environment.FailFast(
                Environment.GetResourceString("ExecutionContext_ExceptionInAsyncLocalNotification"),
                ex);
        }
    }
}

其中一个回调抛出一个异常,导致 OnAsyncLocalContextChanged 在其 try/catch 中调用 Environment.FailFast,这会将一个条目写入事件日志并立即终止应用程序。

由于您可以在 ReportViewer 崩溃之前单击按钮 a 运行dom 次,因此此问题具有竞争条件的所有特征。目前,我们知道如何避免它。就我而言,我需要在刷新报表之前设置显示模式。对您来说,取消 ReportRefresh 事件可以避免双重处理并解决您的问题,即使您并不知道确切原因。也许其他人愿意进一步研究它。

它对其他人的价值:ReportViewer 控件在处理后台呈现时似乎存在错误。我决定监听 RenderingBeginRenderingComplete 事件,跟踪并发渲染尝试的次数如下:

private int _isRendering = 0;

// Constructor code
reportViewer.RenderingBegin += ReportViewer_RenderingBegin;
reportViewer.RenderingComplete += ReportViewer_RenderingComplete;

private void ReportViewer_RenderingComplete(object sender, RenderingCompleteEventArgs e)
{
    _isRendering--;
}

private void ReportViewer_RenderingBegin(object sender, System.ComponentModel.CancelEventArgs e)
{
    _isRendering++;
}

我希望 ReportViewer 永远不会同时呈现多次。一般而言,它似乎保护自己免受此类情况的影响;即 _isRendering 总是 0 或 1。

我注意到调用 SetDisplayMode() 似乎没有取消任何正在进行的渲染,导致 _isRendering 变得大于 1。例如,以下代码行将导致 _isRendering 等于 2:

reportViewer.SetDisplayMode(DisplayMode.PrintLayout);
reportViewer.SetDisplayMode(DisplayMode.Normal);

这将 'nicely' 与@Blake 的研究联系起来,他指出很可能是某些竞争条件造成了麻烦。

因此,虽然这不是一个现成的答案,但我建议检查您的报告是否正在同时呈现。如果是这样,请特别确保检查对 SetDisplayMode().

的(重复)调用