Winforms Operación no válida a través de subprocesos: Se tuvo acceso al control 'control' desde un subproceso distinto a aquel en que lo creó

En Winforms solo existe un proceso para la interfaz gráfica, el llamado hilo o proceso principal que puede ser accedido desde cualquiera de las clases que extienden y usan la clase o System.Windows.Forms.Control sus subcomponentes. Si intentas acceder este hilo o proceso desde otro, provocarás la excepción.

Por ejemplo, analiza el siguiente código:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading; 

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

        private void Form1_Load(object sender, EventArgs e)
        {
            Thread TypingThread = new Thread(delegate () {
                // Blockear el hilo por 5 segundos
                uiLockingTask();

                // Cambia el estado de los botones dentro del hilo TypingThread
                // Esto lanzará una excepción
                button1.Enabled = true;
                button2.Enabled = false; 
            });

            // Cambia el estado de los botones en el hilo principal de la interfaz gráfica
            button1.Enabled = false;
            button2.Enabled = true;

            TypingThread.Start();
        }

        /**
         * La tarea pesada que bloquea la interfaz gráficas
         *
         */
        private void uiLockingTask(){
            Thread.Sleep(5000);
        }
    }
}

Cómo resolverla?

De acuerdo a la situación de tu proceso de desarrollo como el tiempo, la dificultad del proyecto, podrás solucionar tu problema desde 2 maneras:

A. La manera rápida y no tan limpia

Necesitarás verificar si necesitas invocar una función para hacer cambios al control, si este es el caso puedes delegar un método como se muestra en el siguiente ejemplo:

private void button1_Click(object sender, EventArgs e)
{
    Thread TypingThread = new Thread(delegate () {

        heavyBackgroundTask();

        // Cambiar el estado de los botones dentro del hilo TypingThread
        // Esto no generará excepciones de nuevo !
        if (button1.InvokeRequired)
        {
            button1.Invoke(new MethodInvoker(delegate
            {
                button1.Enabled = true;
                button2.Enabled = false;
            }));
        }
    });

    // Cambiar el estado de los botones en el hilo principal
    button1.Enabled = false;
    button2.Enabled = true;

    TypingThread.Start();
}

Este ejemplo no generará excepciones nuevamente. La razón por la que este método no es tan limpio, es porque si estás ejecutando una tarea en otro hilo, quiere decir que es pesada, al hacer esto no se bloqueará la interfaz pero sigue en el mismo hilo. Debes siempre usar un hilo diferente en vez del proceso principal.

B. La manera correcta pero no tan rápida

Sigues aquí? Vaya, usualmente nadie lee el segundo método, sin embargo sigamos. En este caso, para hacerlo de la manera correcta, necesitarás pensar y rediseñar cuidadosamente el algoritmo de tu aplicación pues el modelo actual está fallando de alguna manera. En nuestro ejemplo, nuestro proceso pesado es una simple función que espera por 5 segundos y luego te permite continuar. En la vida real, tus tareas pesadas pueden ser realmente arduas para el PC y no deberían ser ejecutadas en el hilo principal.

Asi que, cual es la manera correcta de hacerlo? Ejecuta tu tarea pesada en otro hilo y envia "mensajes" del segundo hilo al hilo principal de la interfaz gráfica para modificar los controles allí. Esto puede ser logrado gracias al AsyncOperationManager de .NET que obtiene o modifica la sincronización del contexto de operaciones asíncronas.

Lo primero que necesitas hacer es crear una clase que será enviada como un tipo de respuesta del segundo hilo al principal. Como mencionamos anteriormente, piensa en este modelo como callbacks o devoluciones de llamada, donde un callback declarado en el hilo principal será ejecutado cuando algo pasa en otro hilo. Esta clase puede ser creada como quieras, por ejemplo:

public class HeavyTaskResponse
{
    private readonly string message;

    public HeavyTaskResponse(string msg)
    {
        this.message = msg;
    }

    public string Message { get { return message; } }
}

La clase es obligatoria pues querrás probablemente enviar multiple información del segundo hilo al principal como texto, números u objetos etc. Ahora es importante incluir el namespace para acceder al contexto de sincronización así que no olvides agregar el siguiente tipo al inicio de tu clase:

using System.ComponentModel;

Ahora lo siguiente que debes pensar es en como trabajaran y que harán los callbacks declarados en el primer hilo y que serán ejecutados desde el segundo. Los callbacks necesitan ser declarados como un EventHandler del tipo de la clase de respuesta creada anteriormente (en nuestro ejemplo  HeavyTaskResponse) y estarán al principio obviamente vacías. Estos callbacks necesitan ser publicos pues necesitas agregar el callback durante la declaración de una nueva instancia de la clase HeavyTask. Nuestro proceso se ejecutará indefinidamente, así que deberás crear una variable booleana bandera accesible a nivel de la clase (que en este caso se llama HeavyProcessStopped). Luego debes exponer una instancia de solo lectura del sincronizador de contexto accessible por la clase. Es muy importante que actualices el valor de esta variable (SyncContext) en el constructor de la clase con el valor de AsyncOperationManager.SynchronizationContext, de otra manera el punto principal de esta tarea no se usará.

Luego viene la lógica de los procesos, en este caso nuestra clase HeavyTask se expone a si misma como una clase que ejecuta una tarea que consume bastantes recursos y que corre en el fondo en otro proceso. Este proceso debe ser iniciado y parado como un timer, así que necesitarás crear 3 métodos StartStop and Run. El método Start ejecuta un proceso con la función Run como argumento. El método Run se ejecuta indefinidamente en un ciclo while hasta que la variable bandera HeavyProcessStopped sea declarada como verdadera por el método Stop.

Dentro del bucle while puedes ejecutar tu tarea que bloquearía el hilo principal de la interfaz gráfica sin ningun problema pues ahora está siendo ejecuta en otro proceso. Ahora, si necesitas actualizar un control del segundo proceso, como por ejemplo unos botones, no lo harás directamente en el segundo proceso sino que enviarás una "notificación" al proceso principal de que debería actualizar los botones usando los callbacks declarados. Estos callbacks son ejecutados gracias al método SyncContext.Post que recibe como primer argumento una funcion cómo SendOrPostCallback (que a su vez recibe una instancia de la clase respuesta como primer argumento con información que debe ser enviada desde el segundo proceso al primero) y el objeto sender que en este caso puede ser nulo. Este callback necesita ser un método accesible en el segundo proceso que recibe como primer argument la instancia de HeavyTask si el callback real existe. En este ejemplo ejecutaremos 2 callbacks:

Nota

Recuerda que los callbacks pueden ser ejecutados multiples veces de acuerdo a tus necesidades, nosotros solo lo ejecutaremos una vez en el ejemplo.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class HeavyTask
{
    // Bandera booleana que indica cuando el proceso está siendo ejecutado o ha sido detenido
    private bool HeavyProcessStopped;

    // Expone el contexto de sincronización en la clase entera 
    private readonly SynchronizationContext SyncContext;

    // Crear los 2 contenedores de callbacks
    public event EventHandler<HeavyTaskResponse> Callback1;
    public event EventHandler<HeavyTaskResponse> Callback2;

    // Constructor de la clase HeavyTask
    public HeavyTask()
    {
        // Importante actualizar el valor de SyncContext en el constructor con
        // el valor de SynchronizationContext del AsyncOperationManager
        SyncContext = AsyncOperationManager.SynchronizationContext;
    }

    // Método para iniciar el proceso
    public void Start()
    {
        Thread thread = new Thread(Run);
        thread.IsBackground = true;
        thread.Start();
    }

    // Método para detener el proceso
    public void Stop()
    {
        HeavyProcessStopped = true;
    }

    // Método donde la lógica principal de tu tarea se ejecuta
    private void Run()
    {
        while (!HeavyProcessStopped)
        {
            // En nuestro ejemplo solo esperaremos 2 segundos y eso es todo
            // En tu clase obviamente se ejecutará la tarea pesada
            Thread.Sleep(2000);

            // Ejecuta el primer callback desde el proceso de fondo al hilo principal (el de la interfaz gráfica)
            // El primer callback activa el primer boton !
            SyncContext.Post(e => triggerCallback1(
                new HeavyTaskResponse("Algo de información de prueba")
            ), null);

            // Esperar otros 2 segundos para más tareas pesadas.
            Thread.Sleep(2000);

            // Ejecutar segundo callback desde el segundo proceso al primero
            SyncContext.Post(e => triggerCallback2(
                new HeavyTaskResponse("Más información")
            ), null);
            
            // La tarea heavy task finaliza, así que hay que detenerla.
            Stop();
        }
    }


    // Métodos que ejecutan los callback si y solo si fueron declarados durante la instanciación de la clase HeavyTask
    private void triggerCallback1(HeavyTaskResponse response)
    {
        // Si el primer callback existe, ejecutarlo con la información dada
        Callback1?.Invoke(this, response);
    }

    private void triggerCallback2(HeavyTaskResponse response)
    {
        // Si el segundo callback existe, ejecutarlo con la información dada
        Callback2?.Invoke(this, response);
    }
}

Ahora que la tarea pesada puede notificar al hilo principal cuando un control debería ser actualizado, necesitarás declarar los callbacks al crear la nueva instancia de tu tarea pesada:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

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

        private void Form1_Load(object sender, EventArgs e)
        {
            // Crear una instancia de la tarea pesada
            HeavyTask hvtask = new HeavyTask();

            // Puedes crear multiples callbacks o solo uno
            hvtask.Callback1 += CallbackChangeFirstButton;
            hvtask.Callback2 += CallbackChangeSecondButton;

            hvtask.Start();
        }

        private void CallbackChangeFirstButton(object sender, HeavyTaskResponse response)
        {
             // Acceder al boton desde el hilo principal :)
            button1.Enabled = true;

            // Imprime: Algo de información de prueba
            Console.WriteLine(response.Message);
        }

        private void CallbackChangeSecondButton(object sender, HeavyTaskResponse response)
        {
            // Acceder al boton desde el hilo principal :)
            button2.Enabled = false;

            // Imprime: Más información
            Console.WriteLine(response.Message);
        }
    }
}

En este ejemplo fue bastante sencillo crear otra clase para ejecutar la tarea pesada que en el ejemplo inicial era simplemente una función en el proceso principal. Este método es el recomendado pues hace tu código sostenible, dinámico y útil. Si no lo entiendes con este ejemplo, te recomedamos leer otro asombroso artículo acerca del mismo método en otro blog aquí.

Que te diviertas !

Esto podría ser de tu interes

Conviertete en un programador más sociable