John Sheares
John Sheares

Reputation: 681

Cross-thread operation not valid when reading property

I am getting this error when I try to read a property from a custom panel control. The property returns the value of a textbox within the panel. How do I read the property that returns the value of the textbox control from another thread? Sample of my property code is below. I am not worried about the setter.

Here is the eaxct error message: Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.

public string Header
{
get
{
   return _HeaderComboBox.Text;
}
set
{
   _HeaderComboBox.Text = value;
}
}

Upvotes: 4

Views: 7086

Answers (5)

FalcoGer
FalcoGer

Reputation: 2457

I found the answere in a similar thread: How do I update the GUI from another thread?

I expanded upon it with this custom class that extends System.Windows.Forms.Control object with SetPropertyThreadSafe and GetPropertyThreadSafe. To use this extention, all you have to do is to include the namespace like so:

using ControlExtention;

Here is my code:

using System;
using System.Linq.Expressions;
using System.Windows.Forms;

namespace ControlExtention
{
    public static class ControlExtentions
    {
        // https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread
        // Usage: myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile
        private delegate void SetPropertyThreadSafeDelegate<TResult>(Control @this, Expression<Func<TResult>> property, TResult value);

        /// <summary>
        /// Set property from a Thread other than the UI Thread safely.
        /// Use with Lambda-Expression: ControlObject.SetPropertyThreadSafe(() => ControlObject.Property, NewPropertyValue);
        /// </summary>
        /// <typeparam name="TResult">Do not set.</typeparam>
        /// <param name="this">Use lambda expression.</param>
        /// <param name="property">Use lambda expression.</param>
        /// <param name="value">Use lambda expression.</param>
        public static void SetPropertyThreadSafe<TResult>(this Control @this, Expression<Func<TResult>> property, TResult value)
        {
            System.Reflection.PropertyInfo propertyInfo = (property.Body as MemberExpression).Member as System.Reflection.PropertyInfo;

            // check ob property überhaupt ein teil von this ist
            if (propertyInfo == null || [email protected]().IsSubclassOf(propertyInfo.ReflectedType) || @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
            {
                throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
            }

            if (@this.InvokeRequired)
            {
                @this.Invoke(new SetPropertyThreadSafeDelegate<TResult>(SetPropertyThreadSafe), new object[] { @this, property, value });
            }
            else
            {
                @this.GetType().InvokeMember(propertyInfo.Name, System.Reflection.BindingFlags.SetProperty, null, @this, new object[] { value });
            }
        }

        private delegate TResult GetPropertyThreadSafeDelegate<TResult>(Control @this, Expression<Func<TResult>> property);

        /// <summary>
        /// Get property from a Thread other than the UI Thread safely.
        /// Use with Lambda-Expression: value = ControlObject.GetPropertyThreadSafe(() => ControlObject.Property);
        /// </summary>
        /// <typeparam name="TResult">Do not set.</typeparam>
        /// <param name="this">Use lambda expression.</param>
        /// <param name="property">Use lambda expression.</param>
        /// <param name="value">Use lambda expression.</param>
        public static TResult GetPropertyThreadSafe<TResult>(this Control @this, Expression<Func<TResult>> property)
        {
            System.Reflection.PropertyInfo propertyInfo = (property.Body as MemberExpression).Member as System.Reflection.PropertyInfo;

            // check ob property überhaupt ein teil von this ist
            if (propertyInfo == null || [email protected]().IsSubclassOf(propertyInfo.ReflectedType) || @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
            {
                throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
            }

            if (@this.InvokeRequired)
            {
                return (TResult)@this.Invoke(new GetPropertyThreadSafeDelegate<TResult>(GetPropertyThreadSafe), new object[] { @this, property });
            }
            else
            {
                return (TResult)@this.GetType().InvokeMember(propertyInfo.Name, System.Reflection.BindingFlags.GetProperty, null, @this, new object[] { });
            }
        }
    }
}

Upvotes: 0

IAbstract
IAbstract

Reputation: 19881

MSDN sample using BeginInvoke

This is how I would implement the sample based on the getter snippet you posted:

public string Header {
    get {
        string text = string.Empty;
        _HeaderComboBox.BeginInvoke(new MethodInvoker(delegate {
            text = _HeaderComboBox.Text;
        }));
        return text;
    }

    set {
        _HeaderComboBox.Text = value;
    }
}

There are more elegant methods, however, this is a general example for you.

Upvotes: 8

user153498
user153498

Reputation:

You cannot access WinForms controls on any thread other than the UI thread, A.K.A. the one it was created on, because of cross-threading issues, race conditions, etc. To solve this, you have to run any commands you want to run on the UI thread. This can be done by using the Invoke method:

public void InvokeExample()
{
    if (InvokeRequired)
    {
        // Invoke this method on the UI thread using an anonymous delegate
        Invoke(new MethodInvoker(() => InvokeExample()));
        return;
    }

    string header = Control.Header;
}

Upvotes: 2

HalloDu
HalloDu

Reputation: 907

You have to outsource your cross-thread code into a separate method, make a delegate of it and then Invoke it on a Control in the Thread you want to change. You could also use a closure instead of a delegate+method.

Upvotes: 0

casperOne
casperOne

Reputation: 74530

You need to marshal the call back to the UI thread in order to access the property.

Before .NET 2.0, you had to call the Invoke method on the Control class in order to marshal the call to the Text proprety.

In .NET 2.0 and after, if your background thread has access to the SynchronizationContext for the UI thread, then you can call the Send method to marshal the call back to the UI.

Note, that if you didn't have to wait for the result of the calls (as you do here, since you want the result of the call to the Text property), you could call BeginInvoke and Post on Control and SynchronizationContext respectively.

Upvotes: 3

Related Questions