c# - winform - system invalidoperationexception opération inter threads non valide




Opération inter-thread non valide: contrôle accédé à partir d'un thread autre que le thread sur lequel il a été créé (14)

J'ai un scénario. (Windows Forms, C #, .NET)

  1. Il existe un formulaire principal qui héberge un certain contrôle utilisateur.
  2. Le contrôle utilisateur effectue une opération de données lourde, de sorte que si j'appelle directement la méthode UserControl_Load , l'interface utilisateur ne répond plus pendant la durée de l'exécution de la méthode load.
  3. Pour surmonter cela, je charge des données sur différents threads (en essayant de modifier le code existant aussi peu que je peux)
  4. J'ai utilisé un thread d'arrière-plan qui chargera les données et quand cela sera fait, j'indiquerai à l'application qu'elle a fait son travail.
  5. Maintenant est venu un vrai problème. Toute l'interface utilisateur (formulaire principal et ses contrôles utilisateur enfant) a été créée sur le thread principal principal. Dans la méthode LOAD de la commande usercontrol, j'obtiens des données basées sur les valeurs de certains contrôles (comme textbox) sur userControl.

Le pseudocode ressemblerait à ceci:

CODE 1

UserContrl1_LoadDataMethod()
{
    if (textbox1.text == "MyName") // This gives exception
    {
        //Load data corresponding to "MyName".
        //Populate a globale variable List<string> which will be binded to grid at some later stage.
    }
}

L'exception a été donnée

L'opération cross-thread n'est pas valide: le contrôle a été accédé à partir d'un thread autre que le thread sur lequel il a été créé.

Pour en savoir plus à ce sujet, j'ai fait un peu de googling et une suggestion est apparue comme en utilisant le code suivant

CODE 2

UserContrl1_LoadDataMethod()
{
    if (InvokeRequired) // Line #1
    {
        this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod));
        return;
    }

    if (textbox1.text == "MyName") // Now it wont give an exception
    {
    //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be binded to grid at some later stage
    }
}

MAIS MAIS MAIS ... il me semble que je suis de retour à la case départ. L'application redevient insensible. Cela semble être dû à l'exécution de la ligne # 1 si condition. La tâche de chargement est à nouveau effectuée par le thread parent et non par le tiers généré.

Je ne sais pas si j'ai perçu ce droit ou ce qui est mal. Je suis nouveau dans le threading.

Comment puis-je résoudre ceci et quel est l'effet de l'exécution de la ligne # 1 si bloquer?

La situation est la suivante : je veux charger des données dans une variable globale basée sur la valeur d'un contrôle. Je ne veux pas modifier la valeur d'un contrôle du thread enfant. Je ne vais jamais le faire à partir d'un fil d'enfant.

Donc seulement accéder à la valeur afin que les données correspondantes peuvent être récupérées à partir de la base de données.


Modèle Threading dans l'interface utilisateur

Veuillez lire le modèle Threading dans les applications d'interface utilisateur afin de comprendre les concepts de base. Le lien accède à la page qui décrit le modèle de thread WPF. Toutefois, Windows Forms utilise la même idée.

Le fil de l'interface utilisateur

  • Il n'y a qu'un seul thread (thread UI), qui est autorisé à accéder à System.Windows.Forms.Control et ses membres de sous-classes.
  • Tenter d'accéder au membre de System.Windows.Forms.Control partir d'un thread différent du thread d'interface utilisateur provoquera une exception de threads croisés.
  • Comme il n'y a qu'un seul thread, toutes les opérations d'interface utilisateur sont mises en file d'attente en tant qu'éléments de travail dans ce thread:

Méthodes BeginInvoke et Invoke

  • La charge de calcul de la méthode invoquée doit être faible, tout comme le surcoût des méthodes du gestionnaire d'événements, car le thread d'interface utilisateur y est utilisé, le même que celui qui gère les entrées utilisateur. Indépendamment si c'est System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke .
  • Pour effectuer une opération coûteuse, utilisez toujours un thread séparé. Depuis .NET 2.0 BackgroundWorker est dédié à l'exécution d'opérations coûteuses dans les Windows Forms. Cependant, dans les nouvelles solutions, vous devez utiliser le modèle async-await comme décrit here .
  • Utilisez les méthodes System.Windows.Forms.Control.Invoke ou System.Windows.Forms.Control.BeginInvoke uniquement pour mettre à jour une interface utilisateur. Si vous les utilisez pour des calculs lourds, votre application bloquera:

Invoquer

BeginInvoke

Solution de code

Lire les réponses à la question Comment mettre à jour l'interface graphique d'un autre thread en C #? . Pour C # 5.0 et .NET 4.5, la solution recommandée est here .


Action y; // déclaré à l'intérieur de la classe

label1.Invoke (y = () => label1.Text = "texte");


Dans le même sens que les réponses précédentes, mais une très courte addition qui permet d'utiliser toutes les propriétés de contrôle sans avoir une exception d'invocation de thread croisée.

Méthode d'assistance

/// <summary>
/// Helper method to determin if invoke required, if so will rerun method on correct thread.
/// if not do nothing.
/// </summary>
/// <param name="c">Control that might require invoking</param>
/// <param name="a">action to preform on control thread if so.</param>
/// <returns>true if invoke required</returns>
public bool ControlInvokeRequired(Control c, Action a)
{
    if (c.InvokeRequired) c.Invoke(new MethodInvoker(delegate
    {
        a();
    }));
    else return false;

    return true;
}

Exemple d'utilisation

// usage on textbox
public void UpdateTextBox1(String text)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(textBox1, () => UpdateTextBox1(text))) return;
    textBox1.Text = ellapsed;
}

//Or any control
public void UpdateControl(Color c, String s)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(myControl, () => UpdateControl(c, s))) return;
    myControl.Text = s;
    myControl.BackColor = c;
}

Il existe deux options pour les opérations croisées.

Control.InvokeRequired Property 

et le second est d'utiliser

SynchronizationContext Post Method

Control.InvokeRequired n'est utile que lorsque vous utilisez des contrôles hérités de la classe Control tandis que SynchronizationContext peut être utilisé n'importe où. Certaines informations utiles sont les liens suivants

Interface de mise à jour de Thread Thread | .Net

Interface utilisateur de mise à jour croisée avec SynchronizationContext | .Net


J'ai trouvé un besoin pour cela lors de la programmation d'un contrôleur d'application iOS-Phone monotouch dans un projet de prototype Winforms studio visuel en dehors de xamarin stuidio. Préférant programmer autant que possible VS dans le studio xamarin, je voulais que le contrôleur soit complètement découplé du framework de téléphone. De cette façon, la mise en œuvre de ce pour d'autres cadres comme Android et Windows Phone serait beaucoup plus facile pour les utilisations futures.

Je voulais une solution où l'interface graphique pourrait répondre aux événements sans avoir à gérer le code de commutation croisé derrière chaque clic de bouton. Fondamentalement, laissez le contrôleur de classe gérer cela pour garder le code du client simple. Vous pourriez avoir de nombreux événements sur l'interface graphique où, comme si vous pouviez le gérer à un endroit dans la classe serait plus propre. Je ne suis pas un expert en la matière, laissez-moi savoir si cela est défectueux.

public partial class Form1 : Form
{
    private ExampleController.MyController controller;

    public Form1()
    {          
        InitializeComponent();
        controller = new ExampleController.MyController((ISynchronizeInvoke) this);
        controller.Finished += controller_Finished;
    }

    void controller_Finished(string returnValue)
    {
        label1.Text = returnValue; 
    }

    private void button1_Click(object sender, EventArgs e)
    {
        controller.SubmitTask("Do It");
    }
}

Le formulaire d'interface graphique ignore que le contrôleur exécute des tâches asynchrones.

public delegate void FinishedTasksHandler(string returnValue);

public class MyController
{
    private ISynchronizeInvoke _syn; 
    public MyController(ISynchronizeInvoke syn) {  _syn = syn; } 
    public event FinishedTasksHandler Finished; 

    public void SubmitTask(string someValue)
    {
        System.Threading.ThreadPool.QueueUserWorkItem(state => submitTask(someValue));
    }

    private void submitTask(string someValue)
    {
        someValue = someValue + " " + DateTime.Now.ToString();
        System.Threading.Thread.Sleep(5000);
//Finished(someValue); This causes cross threading error if called like this.

        if (Finished != null)
        {
            if (_syn.InvokeRequired)
            {
                _syn.Invoke(Finished, new object[] { someValue });
            }
            else
            {
                Finished(someValue);
            }
        }
    }
}

Je sais qu'il est trop tard maintenant. Cependant, même aujourd'hui, si vous éprouvez des difficultés à accéder aux contrôles de thread croisés? C'est la réponse la plus courte jusqu'à la date: P

Invoke(new Action(() =>
                {
                    label1.Text = "WooHoo!!!";
                }));

C'est ainsi que j'accède à n'importe quel contrôle de formulaire à partir d'un thread.



Les contrôles dans .NET ne sont généralement pas thread-safe. Cela signifie que vous ne devez pas accéder à un contrôle d'un thread autre que celui où il réside. Pour contourner cela, vous devez invoquer le contrôle, ce qui est ce que votre deuxième échantillon tente.

Cependant, dans votre cas, tout ce que vous avez fait est de passer la méthode de longue durée au thread principal. Bien sûr, ce n'est pas vraiment ce que vous voulez faire. Vous devez le repenser un peu afin que tout ce que vous faites sur le thread principal soit de définir une propriété rapide ici et là.


Selon le commentaire de mise à jour de Prerak K (depuis supprimé):

Je suppose que je n'ai pas bien présenté la question.

La situation est la suivante: Je veux charger des données dans une variable globale basée sur la valeur d'un contrôle. Je ne veux pas modifier la valeur d'un contrôle du thread enfant. Je ne vais jamais le faire à partir d'un fil d'enfant.

Donc seulement accéder à la valeur afin que les données correspondantes peuvent être récupérées à partir de la base de données.

La solution que vous voulez alors devrait ressembler à:

UserContrl1_LOadDataMethod()
{
    string name = "";
    if(textbox1.InvokeRequired)
    {
        textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; }));
    }
    if(name == "MyName")
    {
        // do whatever
    }
}

Faites votre traitement sérieux dans le thread séparé avant de tenter de revenir au thread du contrôle. Par exemple:

UserContrl1_LOadDataMethod()
{
    if(textbox1.text=="MyName") //<<======Now it wont give exception**
    {
        //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be
        //bound to grid at some later stage
        if(InvokeRequired)
        {
            // after we've done all the processing, 
            this.Invoke(new MethodInvoker(delegate {
                // load the control with the appropriate data
            }));
            return;
        }
    }
}

Suivez le moyen le plus simple (à mon avis) de modifier les objets d'un autre thread:

using System.Threading.Tasks;
using System.Threading;

namespace TESTE
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Action<string> DelegateTeste_ModifyText = THREAD_MOD;
            Invoke(DelegateTeste_ModifyText, "MODIFY BY THREAD");
        }

        private void THREAD_MOD(string teste)
        {
            textBox1.Text = teste;
        }
    }
}

Voici une alternative si l'objet avec lequel vous travaillez n'a pas

(InvokeRequired)

C'est utile si vous travaillez avec le formulaire principal dans une classe autre que le formulaire principal avec un objet qui est dans le formulaire principal, mais qui n'a pas InvokeRequired

delegate void updateMainFormObject(FormObjectType objectWithoutInvoke, string text);

private void updateFormObjectType(FormObjectType objectWithoutInvoke, string text)
{
    MainForm.Invoke(new updateMainFormObject(UpdateObject), objectWithoutInvoke, text);
}

public void UpdateObject(ToolStripStatusLabel objectWithoutInvoke, string text)
{
    objectWithoutInvoke.Text = text;
}

Cela fonctionne comme ci-dessus, mais c'est une approche différente si vous n'avez pas d'objet avec invokerequired, mais avez accès au MainForm


Vous devez regarder l'exemple Backgroundworker:
BackgroundWorker Surtout comment il interagit avec la couche d'interface utilisateur. Basé sur votre publication, cela semble répondre à vos problèmes.


Même question: comment-mettre-à-jour-le-gui-de-un-autre-fil-en-c

Deux façons:

  1. Renvoie la valeur dans e.result et l'utilise pour définir la valeur de la zone de texte yout dans l'événement backgroundWorker_RunWorkerCompleted

  2. Déclarez une variable pour contenir ce type de valeurs dans une classe séparée (qui fonctionnera comme détenteur de données). Créez une instance statique de cette classe et vous pouvez y accéder par n'importe quel thread.

Exemple:

public  class data_holder_for_controls
{
    //it will hold value for your label
    public  string status = string.Empty;
}

class Demo
{
    public static  data_holder_for_controls d1 = new data_holder_for_controls();
    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(perform_logic);
        Thread t1 = new Thread(ts);
        t1.Start();
        t1.Join();
        //your_label.Text=d1.status; --- can access it from any thread 
    }

    public static void perform_logic()
    {
        //put some code here in this function
        for (int i = 0; i < 10; i++)
        {
            //statements here
        }
        //set result in status variable
        d1.status = "Task done";
    }
}

this.Invoke(new MethodInvoker(delegate
            {
                //your code here;
            }));




invoke