Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

Introduction

This is a portion of how the ReportViewer looks like in my Winforms application which is targeting .NET framework 4.6.1.

enter image description here

The OK button calls the btnOk_Click event while the refresh button (Double-green arrows in a circle) calls the ReportViewer event which in itself calls btnOK_Click event with null parameters. The code below illustrates it

Code

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
    }

Problem

My application crashes after clicking Refresh button in ReportViewer a few times.

enter image description here

This is what I found in the Event Viewer > Windows Logs > Application

Message: An exception was not handled in an AsyncLocal notification callback. I tried googling for the error but came up short.

enter image description here

Clues

  1. This problem does not happen in debug mode (Visual Studio 2015)
  2. This problem does not happen when I rapidly click the OK button.
  3. This problem does not happen when I add redundant/test codes (e.g., MessageBox.Show()) after this.rpvCustomReport.RefreshReport(); line in btnOk_Click event. But when I add them before that line, the problem happens. This was how I concluded that the this.rpvCustomReport.RefreshReport(); was causing the problem.

Questions

  1. Why did that problem occur?
  2. What are the steps I should perform to be able to debug this kind of problem in the future?

Workaround

In order to fix this, I have to cancel the event before calling 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
}

I still don't understand why I need to cancel the default behavior. It doesn't seem like a good fix.

like image 635
John Evans Solachuk Avatar asked Jan 25 '17 08:01

John Evans Solachuk


1 Answers

I posted this response to your question on MSDN also.

This has something to do with the ReportViewer's internal asynchronous rendering when you cause it to cancel in the middle of its current operation and start again. I ran into it by loading my report and then immediately setting the display mode to print layout. Experimenting, this can be reduced to a repeatable failure by adding a button to a form with the following code and then clicking it repeatedly (caveat is, as you noted, the issue does not occur when running in the debugger):

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

In your case, clicking the ReportViewer's refresh button causes the report to fire off its internal refresh routine. That code looks like this (extracted using JetBrains dotPeek, although Microsoft has open-sourced this now, so you can find on the MS code reference site):

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);
  }
}

Notice that the ReportRefresh event is raised and, if you do not cancel this event, the ReportViewer continues to process and re-render the report. The code you have in the event handler also tells the ReportViewer to refresh, which basically sets up the same issue of whipsawing the ReportViewer like my code did.

I had originally intended to isolate this further with the idea of filing an official bug report on MS Connect, but I've gone about as far down the rabbit hole as I care to go. What we do know from the call stack is that a thread is switching execution context:

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)

When OnAsyncLocalContextChanged fires, it attempts to process callbacks for change notification:

[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);
        }
    }
}

One of those callbacks is tossing an exception causing OnAsyncLocalContextChanged to call Environment.FailFast in its try/catch, which writes an entry to the event log and immediately terminates the application.

Since you can click on the button a random number of times before the ReportViewer blows up, this issue has all the hallmarks of a race condition. For now, we know how to avoid it. In my case, I need to set the display mode before I refresh the report. For you, canceling the ReportRefresh event avoids double-processing and solves your problem, even though you did not know exactly why. Maybe someone else out there cares to look into it further.

like image 182
Blake Avatar answered Sep 25 '22 11:09

Blake