Search code examples
c#winformschartspaintonpaint

Winforms OnPaint not being called after Exception in Chart


Hello devs of the internet!

The Winforms.DataVisualization.Charting.Chart draws itself as a big red cross if the axes or displayed points take values they should not have.

This happens when an exception occurs inside the OnPaint method. Other questions to this topic advise to make a subclass and override OnPaint. That's what i did and thats how i found out what caused the problem.

The interesting question is: How to i get rid of this error state? I tried settig the properties to valid values and clearing all content, calling Invalidate() and the usual things. The OnPaint method is never called again after the error occured.

Is there some kind of behaviour in winforms that leads to this? Can controls be flagged as "failed beyond recover"? When i move controls above the failed state chart, the red x is redrawn, so it is painted, but not by itself.

I wonder which kind of detail knowledge i am missing here.

PS: SetStyle does not help, OnPaint is called before the error happens and imlementing it does not solve it.


Appendix A: Recompiling I disassembled the source code of the whole Winforms.DataVisualization.Charting assembly and recompiled it. By commenting out the throw; in the charts OnPaint, there method does not leave with an expection but just draws an "error has occured"-image itself. In this case, the repainting is not disabled forever.

Somehow it looks like Winforms removed controls from the drawing order if they throw an exception.


Solution

  • Debugging the extracted source version of the assembly brought some insight into the problem. First, controls being excluded from OnPaint forever after throwing an exception is normal behaviour according to the link @TnTinMn provided.

    The chart itself however has one codepath in OnPaint that does not reset an internal field that is used to block invalidation while OnPaint is running.

    The following code is a subclass of the chart that catches the OnPaint exception and uses reflection to reset the private field. Thus, if the chart runs into an error state, it can be reset by setting valid values and simply redrawing.

    Also after creating a chart control, make sure to set the Minimum and Maximum properties of the Axes, even the unused. The properties setter does more than just put a value into a field. For me that helped not having the exception thrown.

    /// <summary>
    /// Subclass catches exception thrown from base.OnPaint and thus prevents the red x.
    /// Resets base classes private field "disableInvalidates" via reflection to reallow invalidation.
    /// </summary>
    public class ChartSubclass : Chart
    {
        private readonly System.Reflection.FieldInfo disableInvalidatesField;
    
        public ChartSubclass()
        {
            this.disableInvalidatesField = typeof(Chart).GetField("disableInvalidates",
                System.Reflection.BindingFlags.NonPublic |
                System.Reflection.BindingFlags.Public |
                System.Reflection.BindingFlags.Static |
                System.Reflection.BindingFlags.SetField |
                System.Reflection.BindingFlags.Instance);
        }
    
        protected override void OnPaint(PaintEventArgs e)
        {
            try
            {
                base.OnPaint(e);
            }
            catch (Exception ex)
            {
                this.disableInvalidatesField.SetValue(this, false);
                this.DrawErrorState(e, ex);
            }
        }
    
        /// <summary>
        /// Draws error message.
        /// </summary>
        private void DrawErrorState(PaintEventArgs e, Exception ex)
        {
            var graphics = e.Graphics;
            graphics.FillRectangle(Brushes.White, 0, 0, base.Width, base.Height);
            var layoutRectangle = new RectangleF(3f, 3f, base.Width - 6, base.Height - 6);
            using (var stringFormat = new StringFormat())
            {
                stringFormat.Alignment = StringAlignment.Center;
                stringFormat.LineAlignment = StringAlignment.Center;
                graphics.DrawString(ex.Message, this.Font, Brushes.Black, layoutRectangle, stringFormat);
            }
        }
    }