Arquivo da tag: InvalidOperationException

.NET – Evitando o InvalidOperationException em aplicações Multi Thread Windows Forms

Um problema um pouco comum para quem trabalha com aplicações Windows Forms e rotinas Multi Thread, principalmente quando as rotinas multi thread interagem com os controles. Vamos imaginar uma situação típica, um aplicativo de envio de emails.

Para oferecer uma interface amigável, você expõem campos com valores editáveis, enfim, e no final do formulário, um TextBox Multiline para mostrar o andamento das tarefas, como se fosse uma janela de Output do Visual Studio. Durante a sua rotina, você quer inserir informações do estado da execução do seu algoritmo. Bem, nada mais simples do que adicionar texto ao TextBox da forma mais tradicional possível.

Bom, se fizer isso sem aplicar Multi Thread, o que você verá é a tela parada até o término da execução da rotina, o que não serve para nós. O jeito é executar a mesma em outra thread, certo? Essa é a parte fácil, já que o .NET Framework dispõem de métodos e bibliotecas bem simples para isso. Você põem para executar o aplicativo já rodando sua rotina principal em outra thread e: InvalidOperationException.

A razão do erro é simples, mas nem tanto. Todos os controles Windows são por padrão Thread-Safe. Objetos Thread-Safe só podem ser manipulados por uma thread por vez. Isso evita que estes sejam colocados em estados inválidos ou inconsistentes. São vários os casos, mas se está tendo este problema, provavelmente sabe do que estou falando.

O que é preciso fazer então? Garantir que sua thread acessa o controle de forma que não interfira em outras threads que já o estejam manipulando. Para isso, precisaremos da propriedade InvokeRequired do controle, o método Invoke e o um delegate para criar um CallBack da nossa função. Bom, nada melhor que um exemplo para melhor entender. Vou economizar tempo e usar um da própria Microsoft, com todas as explicações nos comentários:

// Delegate para permitir a chamada assíncrona
// ao método setText
delegate void SetTextCallback(string text);
 
// A thread a ser executada
private Thread demoThread = null;
 
// O evento que segue cria um nova Thread
// para execução do método ThreadProcSafe
private void setTextSafeBtn_Click(object sender, EventArgs e)
{
	this.demoThread =
		new Thread(new ThreadStart(this.ThreadProcSafe));
 
	this.demoThread.Start();
}
 
// Método que faz uma chamada ao SetText
// que faz a verificação de Thread-Safe
private void ThreadProcSafe()
{
	this.SetText("Texto do textbox.");
}
 
// Método que tenta atribuir o texto ao controle textbox1
// de forma "segura"
private void SetText(string text)
{
	// a chamada ao InvokeRequired garante que a thread atual
	// é a mesma que está manipulando o controle no momento
	// Caso não seja a mesma thread,
	// retorna true e faz a chamada ao CallBack.
	// a chamada ao metódo Invoke faz com que a thread
	// que manipula o componente chame o CallBack quando
	// liberar o controle, passando os argumentos
	if (this.textBox1.InvokeRequired)
	{
		SetTextCallback d = new SetTextCallback(SetText);
		this.Invoke(d, new object[] { text });
	}
	else
	{
		this.textBox1.Text = text;
	}
}

Em miúdos, ao tentar alterar a propriedade de texto do controle este já estiver sendo manipulado por outra thread, cria-se um enfileiramento da sua chamada. Assim, quando a thread que ocupa o controle liberá-lo, sua rotina será automaticamente executada.

A parte chata é que isso deverá ser feito para toda e qualquer propriedade que precisar ser alterada no controle sob a execução de um thread que não a principal.

O mesmo exemplo pode ser visto em VB.NET neste link:

http://www.codigofonte.net/dicas/dotnet/222_trabalhando-com-threads-em-formularios-windows

Bom proveito!

Referências:

http://msdn.microsoft.com/en-us/library/ms171728.aspx

C# – System.InvalidOperationException ao varrer coleções

Collections no C#
Collections no C#

A situação

Uma rotina tradicional, principalmente para quem trabalham com coleções. E para quem não sabe, trabalhar com DataTables e DataSets não o livra de usá-las pois estes objetos não passam de coleções, sejam de DataColumns, DataRows ou mesmo DataTables.

Pois bem, você executa um código “normal”, por exemplo, para remover linhas de um DataTable “varrendo” sua coleção de linhas. O código seria algo parecido com isto:

1
2
3
4
5
6
7
8
9
10
public void RemoveProducts(int ID)
{
        DataTable productData = new DataTable();
        productData = GetProducts(ProductID, false, true);
        foreach ( dr in productData.Rows) {
                if ((dr("ID") != ID.Value)) {
                        productData.Rows.Remove(dr);
                }
        }
}

Sem muitos segredos, o método preenche um DataTable e através de um laço foreach, remove as linhas que obedeçam a um determinado critério.

O problema

Qualquer código que trate coleções desta forma será muito “bem” recebido com a seguinte mensagem:

System.InvalidOperationException: Collection was modified; enumeration operation might not execute.

A descrição da Exception é bem genérica, mas para este caso a explicação é que, uma vez que um laço como o foreach analisa uma coleção para promover o laço, ela cria um Enumerador na primeira execução para promover um MoveNext, que fará avançar para próximo item. Se a coleção for modificada, seja por adição ou remoção de itens, o Enumerador se tornará inválido e o aplicativo retornará a Exception acima.

Abaixo segue a explicação da Microsoft para o fato:

After an enumerator is created or after the Reset method is called, an enumerator is positioned before the first element of the collection, and the first call to the MoveNext method moves the enumerator over the first element of the collection.

If MoveNext passes the end of the collection, the enumerator is positioned after the last element in the collection and MoveNext returns false. When the enumerator is at this position, subsequent calls to MoveNext also return false until Reset is called.

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and the next call to MoveNext or Reset throws an InvalidOperationException

Portanto, nada de mexer na coleção durante a execução de um laço foreach OK.

A solução

Se for realmente necessário construir uma rotina que altere a coleção durante a execução do laço, será preciso recorrer ao laço for tracional ao while. De qualquer forma, mesmo no for tradicional, será necessário fazer o laço de forma reversa, já que você não sabe se o último item estará lá durante a execução do laço.

Por isso a melhor forma de contruir o código sem ter problemas é varrer a coleção de trás para frente. No caso do código apresentado acima, uma possível solução seria esta:

1
2
3
4
5
6
for (int i = productData.Rows.Count - 1; i <= 0; i += -1) {
        DataRow dr = productData.Rows(i);
        if ((dr("ID") != ID.Value)) {
            productData.Rows.RemoveAt(i);
        }
    }

Como dito, o laço é feito da última linha para a primeira, tomando o cuidado de sempre referenciar o índice com o valor -1.

Referências

http://msdn.microsoft.com/en-us/library/system.invalidoperationexception.aspx

http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.movenext.aspx

Bom proveito!