简体   繁体   中英

InvalidOperationException when concatenating bound string

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.

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.

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM