Arquivo da tag: Windows Forms

.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

.NET – Habilitando o OpenFileDialog no controle PropertyGrid

Poucou utilizado em sistemas tradicionais, infelizmente, o controle PropertyGrid disponibilizado a partir do .NET Framework 2.0 possui um forma prática de exibir características de um determinado objeto ou controle. Um tutorial bem completo deste pode ser visto aqui.

Tudo certo. Em um de seus usos, fiquei curioso em saber como alguns sistemas, inclusive o Visual Studio e outras IDEs Microsoft conseguir transformar algumas das caixas de valores do PropertyGrid campos OpenFileDialog, ou seja, quando este precisar conter o caminho de um arquivo, que aparecesse o pequeno botão a direta do campo para exibir tal funcionalidade.

Bom, a resposta é simples. A propriedade que irá expor o valor deve conter os seguintes atributos:

[EditorAttribute(typeof(System.Windows.Forms.Design.FileNameEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public string CaminhoDoArquivo { get; set; }

A mágica é feita pelo atributo System.Windows.Forms.Design.FileNameEditor. O projeto precisa referenciar a bibliotecas System.Windows.Forms e System.Design para alcançar as classes mencionadas. A aparência fica dessa forma:

Também é possível adicionar a caixa de mapeamento de diretório, trocando o valor para System.Windows.Forms.Design.FolderNameEditor. O funcionamento do clique nos botões dispensa explicações, bem como a versão do código para VB.NET, certo?

Referências

http://stackoverflow.com/questions/170791/how-can-i-get-an-openfiledialog-in-a-custom-controls-property-grid

Bom proveito!

C# – Trocando a navegação dos controles Windows do TAB para o ENTER

Essa é um pouco velha, mas tão eminente quanto a necessidade de migração de sistemas legados.

Poderia citar inúmeros motivos que levam a essa “cultura” de utilização de sistemas comerciais, mas vou enfatizar alguns:

  • Legado de sistemas orientados a caracter, geralmente feitos em CLIPPER
  • Legado de linguagens de quarta-geração, ou pelo daqueles aplicativos que o tentam ser, como Access e FoxPro
  • Facilidade na utlização do teclado numérico, já que nele além das teclas númericas há também o ENTER facilitando o acesso

O fato é que, é quase certo que os usuários do sistema antigo solicitarão que a navegação entre campos seja trocada do tradicional TAB pelo ENTER.

Para fazer isso em C# (e qualquer outra linguagem .NET, guardadas as devidas conversões), primeiramente é preciso habilitar a propriedade KeyPreview do Formulário principal para que o controle esteja habilitado a interceptar as teclas digitadas.

Depois, para trocar a navegação, bastaria interceptar o evento KeyDown e fazer o tratamento. Mas para fazer um tratamento mais completo, vamos inteceptar o KeyDown para suprimir o ENTER e o KeyUp para trocar a tecla enviada. O código abaixo faz isso:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// inibe a ação do Enter para evitar o comportamento de
/// Accept em alguns casos
private void _ctrl_KeyDown(object sender, KeyEventArgs e)
{
	if (e.KeyCode == Keys.Enter)
		e.SuppressKeyPress = true;
}
/// adiciona a tratativa à tecla Enter e envia o TAB para
/// promover a navegação
private void _ctrl_KeyUp(object sender, KeyEventArgs e)
{
	if (e.KeyCode == Keys.Enter)
	{
		// SendKeys.Send("{TAB}");
		_ctrl.FindForm().SelectNextControl(_ctrl, true, true, true, false);
	}
}

Percebam que no KeyUp há uma parte do código comentada (SendKeys.Send(“{TAB}”);) que faria com que a tecla TAB fosse acionada no lugar do ENTER, porém, é mais elegante usar o método SelectNextControl do objeto Form, que faz a ação sem a necessidade de simular um comportamento, o que em alguns casos pode causar um comportamento não desejado.

Isso resolve o problema. Para tentar ajudar um pouco mais, disponilizo abaixo um método mais completo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// Adiciona o comportamento de navegação através da tecla Enter,
/// da mesma forma como o TAB
/// </summary>
/// <param name="_ctrl">O controle pai que contém os controles em que
/// serão adicionados os eventos</param>
public static void TrocaTABporEnter(Control _ctrl)
{
	if (_ctrl.HasChildren)
	{
		foreach (Control _child in _ctrl.Controls)
		{
			if (_ctrl.HasChildren)
				TrocaTABporEnter(_child);
		}
	}
	else
	{
		///Não funciona para Numeric Up Down
		if (_ctrl is TextBox ||
		_ctrl is MaskedTextBox ||
		_ctrl is ListBox ||
		_ctrl is CheckBox ||
		_ctrl is TabPage ||
		_ctrl is DateTimePicker ||
		_ctrl is ComboBox)
		{
			/// inibe a ação do Enter para evitar o comportamento de
			/// Accept em alguns casos
			_ctrl.KeyDown += delegate(object sender, KeyEventArgs e)
			{
				if (e.KeyCode == Keys.Enter)
					e.SuppressKeyPress = true;
			};
			/// adiciona a tratativa à tecla Enter e envia o TAB para
			/// promover a navegação
			_ctrl.KeyUp += delegate(object sender, KeyEventArgs e)
			{
				if (e.KeyCode == Keys.Enter)
				{
					// SendKeys.Send("{TAB}");
					_ctrl.FindForm().SelectNextControl(_ctrl, true, true, true, false);
				}
			};
		}
	}
}

Este método pode ser chamado em qualquer momento do seu código e faz com que todos os controles filhos do controle passado por parâmetro adiquiram o comportamento, sem a necessidade de mapear o evento para todos os controles.

Como comentado, o código não funciona com alguns controles, em particular o NumericUpDown. Para que isso seja possível, é preciso sobrescrever alguns métodos da classe, mas isso já outra história.

Abraços

Tomás Vásquez
www.tomasvasquez.com.br