none
C# : Application crashes when spamming Refresh button in ReportViewer RRS feed

  • Question

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

    Problem

    My application crashes when I click the Refresh button rapidly.

    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

    Interesting stuff

    1. This problem does not happen in debug mode
    2. This problem does not happen when I spam the OK button.
    3. This problem does not happen when I add a line, MessageBox.Show("Test") after this.rpvCustomReport.RefreshReport(); in btnOk_Click event. But when I add that line before this.rpvCustomReport.RefreshReport();, the problem happens. This was how I concluded that the this.rpvCustomReport.RefreshReport(); was causing the problem.

    Questions

    1. Why did that problem occur and how do I rectify it? (Besides removing the Refresh button, of course)
    2. What are the steps I should perform to be able to debug this kind of problem in the future?

    • Edited by equals2nine Wednesday, January 25, 2017 9:45 AM
    • Moved by DotNet Wang Thursday, January 26, 2017 2:04 AM Report Viewer related
    Wednesday, January 25, 2017 9:44 AM

Answers

  • 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.

    • Marked as answer by equals2nine Wednesday, May 17, 2017 12:56 AM
    Tuesday, May 16, 2017 6:59 PM

All replies

  • Hi equals2nine,

    Based on your description, your case more related to Winfrom Report Viewer control, I will help move your case to Report Controls forum for better support.

    Best regards,

    Kristin


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Thursday, January 26, 2017 2:03 AM
  • 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
    }

    So the first part of this question is solved. However, I still don't understand why I need to cancel the default behavior. It doesn't seem like a good fix.

    It would be great if someone can improve this answer with links to valid sources (e.g, msdn forums, expert blogs, etc.).


    • Marked as answer by equals2nine Friday, January 27, 2017 12:52 AM
    • Edited by equals2nine Friday, February 3, 2017 8:01 AM
    • Unmarked as answer by equals2nine Wednesday, May 17, 2017 12:56 AM
    Thursday, January 26, 2017 6:50 AM
  • 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.

    • Marked as answer by equals2nine Wednesday, May 17, 2017 12:56 AM
    Tuesday, May 16, 2017 6:59 PM
  • For what its worth to other people: there seem to be bugs in the ReportViewer control in handling the background rendering. I decided to listen in on the RenderingBegin and RenderingComplete events, keeping tracking on the number of concurrent render attempts as follows:

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

    I would expect the ReportViewer to never be rendering more than once at the same time. It seems to protected itself from such cases in general; i.e. _isRendering is always either 0 or 1.

    I noticed that calls to SetDisplayMode() do not seem to cancel any renders in progress, resulting in _isRendering to become larger than 1. The following lines of code will for example result in _isRendering to become equal to 2:

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

    This connects 'nicely' with the research of @limelight, who indicated that most likely some race condition is causing trouble.

    So while this is not really a ready made answer, I suggest checking if your report is concurrently rendering. If so, make especially sure to check for (duplicate) calls to SetDisplayMode().

    Monday, January 27, 2020 3:29 PM