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

Mar*_*ark 5 c# reportviewer winforms

介绍

这是 ReportViewer 在我的面向 .NET 框架 4.6.1 的 Winforms 应用程序中的外观的一部分。

在此处输入图片说明

OK 按钮调用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
    }
Run Code Online (Sandbox Code Playgroud)

问题

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

在此处输入图片说明

这是我在“事件查看器”>“Windows 日志”>“应用程序”中找到的内容

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

在此处输入图片说明

线索

  1. 调试模式下不会发生此问题(Visual Studio 2015)
  2. 当我快速单击“确定”按钮时,不会发生此问题。
  3. 当我在事件行添加冗余/测试代码(例如,MessageBox.Show())时,不会发生此问题。但是当我该行之前添加它们时,问题就发生了。这就是我得出的结论是导致问题的原因。 this.rpvCustomReport.RefreshReport();btnOk_Clickthis.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
}
Run Code Online (Sandbox Code Playgroud)

我仍然不明白为什么我需要取消默认行为。这似乎不是一个好的解决办法。

Bla*_*ake 4

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

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

form.ReportViewer.SetDisplayMode(DisplayMode.PrintLayout);
form.ReportViewer.SetDisplayMode(DisplayMode.Normal);
Run Code Online (Sandbox Code Playgroud)

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

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,将引发 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)
Run Code Online (Sandbox Code Playgroud)

当 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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