Search code examples
c#.netdata-bindingexception

Exception in bound Property's Set method not caught by Application.ThreadException event


It appears that exceptions that occur in a property's Set method do not bubble up to the Application's ThreadException event.

We use that event along with the AppDomain.CurrentDomain.UnhandledException event to catch any unexpected mishaps that occur in the application. The exception details are written to a log so our support and development team can better evaluate the issue. Sadly it looks like this Catch All falls short in this particular case.

There are several similar questions on StackOverflow, But no answer addresses the issue of the global exception handling not catching the exception. I already know we can fix it so no exception occurs. We could add a TryCatch block to every setter. We could add the BindingComplete event to each databinding and get the exception that way. But all of that defeats the purpose of having global exception handling which works perfectly in any other case.

To reproduce the issue, simply create a form with a text box, bind the text box to a property and throw an exception in the property's set method. Add the ThreadException and UnhandledException events to the program.cs. Run the program and type in the text box to trigger the exception. The debugger will break on the exception, press Continue (F5) the let the exception bubble up as it would outside of the debugger. Any normal exception would end up in those events, but this one does not.

Form1.cs

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        textBox1.DataBindings.Add("Text", this, "TestValue", true, DataSourceUpdateMode.OnPropertyChanged);            
    }

    private string _TestValue = "";
    public string TestValue
    {
        get{return _TestValue;}
        set
        {
            _TestValue = value;
            throw new Exception("Something bad happened in here");
        }
    }

Program.cs

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
        Application.ThreadException += ThreadExceptionHandler;
        AppDomain.CurrentDomain.UnhandledException += new System.UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

        Application.Run(new Form1());
    }

    private static void ThreadExceptionHandler(object sender, System.Threading.ThreadExceptionEventArgs args)
    {
        try
        {
            //RR.Common.ErrorLogRt.WriteError(args.Exception.StackTrace.ToString(), args.Exception.Message.ToString(), true);
            MessageBox.Show(args.Exception.Message);
        }
        catch
        {
            MessageBox.Show("Error writing to exception log. This program will now terminate abnormally.");
            Application.Exit();
        }
    }

static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
    {
        try
        {
            if (e != null)
            {
                Exception ex = e.ExceptionObject as Exception;
                //RR.Common.ErrorLogRt.WriteError(ex.StackTrace.ToString(), ex.Message.ToString(), true);
                MessageBox.Show(ex.Message);
            }
            else
            {
                MessageBox.Show("Unhandled Error: " + e.ToString());
            }
        }
        catch
        {
            MessageBox.Show("Error writing to exception log. This program will now terminate abnormally.");
            Application.Exit();
        }
    }


 static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        try
        {
            if (e != null && e.Exception != null && e.Exception.InnerException != null)
            {
                //The unobserved exception is always the same, The actual exception that cause it will be the inner exception.
                Exception ex = e.Exception.InnerException;
                MessageBox.Show(e.Exception.Message);
                //RR.Common.ErrorLogRt.WriteError(ex.StackTrace.ToString(), ex.Message.ToString(), true);
            }
            else
            {
                MessageBox.Show("Unhandled Error: " + e.ToString());
            }


        }
        catch
        {
            MessageBox.Show("Error writing to exception log. This program will now terminate abnormally.");
            Application.Exit();
        }
    }



}

Solution

  • Remarks from Binding.FormattingEnabled Property

    Setting this property to true also enables error-handling behavior and causes the BindingComplete event to be raised. The handler of this event can take the appropriate action, based on the success, error, or exceptions in the binding process, by examining the BindingCompleteState property of the BindingCompleteEventArgs parameter.

    The code involved

    internal bool PushData(bool force)
    {
        Exception ex = null;
        if (!force && this.ControlUpdateMode == ControlUpdateMode.Never)
        {
            return false;
        }
        if (this.inPushOrPull && this.formattingEnabled)
        {
            return false;
        }
        this.inPushOrPull = true;
        try
        {
            if (this.IsBinding)
            {
                object value = this.bindToObject.GetValue();
                object propValue = this.FormatObject(value);
                this.SetPropValue(propValue);
                this.modified = false;
            }
            else
            {
                this.SetPropValue(null);
            }
        }
        catch (Exception ex2)
        {
            ex = ex2;
            if (!this.FormattingEnabled)
            {
                throw;
            }
        }
        finally
        {
            this.inPushOrPull = false;
        }
        if (this.FormattingEnabled)
        {
            BindingCompleteEventArgs bindingCompleteEventArgs = this.CreateBindingCompleteEventArgs(BindingCompleteContext.ControlUpdate, ex);
            this.OnBindingComplete(bindingCompleteEventArgs);
            return bindingCompleteEventArgs.Cancel;
        }
        return false;
    }
    

    As you can see, passing 4th parameter as true: DataBindings.Add("Text", this, "TestValue", true is responsible for catching the exception inside PushData and passing it to BindingComplete event. There is no other way (except AppDomain.CurrentDomain.FirstChanceException) to find the exception anywhere else than in BindingComplete if formatting is enabled.