0

I am attempting to bind a TextBox for error logging in Windows Forms.

I bind the TextBox like this:

this.operationEventHistoryTextbox.DataBindings.Add("Text", this.Logger, "OperationLog")

The Logger object is an instance of the Logger class, which contains the following property.

public string OperationLog
{
    get
    {
       return this.operationLog;
    }
    set
    {
       if (this.operationLog != value)
       {
           this.operationLog = value;
           System.ComponentModel.PropertyChangedEventHandler handler = this.PropertyChanged;
           if (handler != null)
           {
               handler(this, new System.ComponentModel.PropertyChangedEventArgs("OperationLog"));
            }
        }
    }
}

And I get the error when calling this.Logger.LogEvent("message"). My LogEvent method contains the following:

public void LogEvent(string msg)
{
    this.OperationLog += System.DateTime.Now + ": " + msg + "\r\n";
}

The InvalidOperationException says that there was an invalid Cross-thread operation, and that the Control operationEventHistoryTextBox was accessed from a thread other than the thread it was created on.

I understand this to mean that I've written parts of my code in a way that wasn't thread-safe, but I don't really understand why, or what to fix.

I could just go around setting all of these functions to invoke rather than be directly called, but I'd like really understand what isn't working.

Update: I've attempted to use System.Threading.ScynchronizationContext to raise the PropertyChanged Event on the correct thread, however, I continue to get the error. Here is the new setter for the property:

set
{
    if (this.operationLog != value)
    {
        System.Threading.SynchronizationContext context = System.Threading.SynchronizationContext.Current
                                                          ?? new System.Threading.SynchronizationContext();
        this.operationLog = value;
        context.Send(
            (s) =>
               {
                   System.ComponentModel.PropertyChangedEventHandler handler = this.PropertyChanged;
                   if (handler != null)
                   {
                       handler(this, new System.ComponentModel.PropertyChangedEventArgs("OperationLog"));
                   }
               },
               null);
   }
}

Am I not correctly creating the SynchronizationContext? Or is there something else at work here?

Update 2: If I replace the call of handler(this, ... ) with handler(null, ... ) or handler(this.OperationLog), the setter will run without errors, but does not actually update the text.

For now I'm using a workaround where I will, instead of using a DataBinding to link the text, just manually do that by adding my own handler to the PropertyChanged Event.

Andrew Schade
  • 808
  • 1
  • 7
  • 14

3 Answers3

2

The problem seems to be that you are calling the LogEvent method from a background thread. Because of the databinding, the text box is then being updated from that background thread resulting in the exception. The solution is to make sure that either the LogEvent method is always executing on the UI thread or - better - the OperationLog setter.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • If I want to ensure that the Setter for `OperationLog` is on the UI thread, How would I go about doing that? If it were in the class of my control, I could use `operationEventHistoryTextbox.InvokeRequired` and a delegate, but since the textbox isn't in scope, that won't work. – Andrew Schade Jul 28 '15 at 17:20
  • @interstellarshadow Try using `Application.Current.Dispatcher.Invoke`. – Daniel Hilgarth Jul 28 '15 at 17:27
  • In `Application.Current.Dispatcher.Invoke,` `Application` cannot resolve. When I set it to `System.Windows.Forms.Application...`, then Current isn't a member of that object – Andrew Schade Jul 28 '15 at 17:47
  • @interstellarshadow You need to use `System.Windows.Application.Current.Dispatcher.Invoke`. – Daniel Hilgarth Jul 28 '15 at 17:53
  • OK. I was having problems finding that, since the System.Windows assembly wasn't a referenced dll in my project--only system.windows.forms (go figure). EDIT: I added the System.Windows assembly and it still doesn't work – Andrew Schade Jul 28 '15 at 17:56
  • System.Windows.Application is a WPF type, and will only work with WPF. I am using Windows Forms, so that won't work. – Andrew Schade Jul 28 '15 at 18:03
  • @interstellarshadow You are right, sorry about that. There seems to be no agreed upon way in doing this in Win Forms if there is no control available. Maybe [this answer](http://stackoverflow.com/a/606184/572644) can help you find your own solution. – Daniel Hilgarth Jul 28 '15 at 18:11
1

You trying update view from another thread. Textbox.Text can't be set from other thread then UI thread

Piotr Pasieka
  • 2,063
  • 1
  • 12
  • 14
0

Ok, I've found workaround that is almost a solution.

I've done the following:

set
{
    if (this.operationLog != value)
    {
        this.operationLog = value;
        System.ComponentModel.PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            System.Action a  = () => handler(this, new PropertyChangedEventArgs("OperationLog"));
            System.Windows.Forms.Form.ActiveForm.Invoke(a);
        }
    }
}

This guarantees that the event is raised on the UI thread, and allows it to update the textbox safely. However, to me, it doesn't feel quite right. While I've done what it needs, there is something that feels like it doesn't fit the Model-View separation that I was looking for. Be that as it may, it is certainly better than manually going operationEventHistoryTextbox.Invoke( ... ).

An interesting drawback is that this can cause errors when the active form does not share threads with what you are working on -- this will include forms in other processes, so alt tabbing will not be allowed.

EDIT: I've found a fix to that problem. Instead of using System.Windows.Forms.Form.ActiveForm, I used System.Windows.Forms.Application.OpenForms[0] to reference the first form within the app. This gives me the context to run an invoke or begin invoke.

Andrew Schade
  • 808
  • 1
  • 7
  • 14