VB .NET - Usando o controle BackgroundWorker
Como realizar operações de processamento de dados em segundo plano(de modo assíncrono) e manter a sua interface com o usuário ativa e respondendo a interações dos usuários ?
Boa pergunta , garoto ...
Uma resposta simples e objetiva seria:
Utilize a classe ou controle BackgroundWorker para gerenciar as interações entre o processo principal e thread ativa.
Qual a vantagem de usar este
controle se posso eu mesmo fazer o serviço através de threads ?
A vantagem é que o componente já oferece eventos que interagem
com outras threads, eventos que você teria que criar via
código. Como exemplo temos o evento DoWork que
é disparado quando é iniciado o trabalho que este terá
que fazer e o evento RunWorkerCompleted que é
acionado quando o processo assíncrono, que está sendo
executado, for terminado.
Veja na tabela abaixo alguns dos
eventos deste controle e sua descrição:
Evento | Descrição |
DoWork | Ocorre quando RunWorkAsync é chamado. Este evento inicia o processamento assíncrono. |
ProgressChanged | Ocorre quando ReportProgress é chamado.Utilizado fazer uma notificação de progressão do processamento. |
RunWorkerCompleted | Ocorre quando a operação assíncrona é encerrada, ou quando uma exceção é disparada. |
No exemplo deste artigo o código que vamos mostrar inicia uma
thread ativa em segundo plano que realiza alguma operação,
reportando o seu progresso à thread principal . A thread
principal tem a opção de cancelar a thread em segundo plano.
Vamos ao que interessa:
Crie um novo projeto no VB .NET chamado segundoPlanoNet (ou algo mais sugestivo) do tipo Windows Application e inclua os seguintes controles no formulário padrão: form1.vb definindo suas propriedades conforme indicado :
O leiaute do formulário após a inclusão dos controles deve ficar como na figura abaixo:
A seguir as telas mostrando :
fig 1.0 | fig 2.0 |
fig 3.0 |
A seguir o código para os eventos do componente BackgroundWorker:
Private
Sub BackgroundWorker1_DoWork(ByVal sender As
System.Object, ByVal e As
System.ComponentModel.DoWorkEventArgs) Handles
atividadeSegundoPlano.DoWork ' ----- O trabalho em segundo plano (background) começa aqui Dim segundoPlano As BackgroundWorker ' ----- Chama a thread em segundo plano (background) segundoPlano = CType(sender, BackgroundWorker) executaTrabalho(segundoPlano) ' ----- Verifica cancelamento If (segundoPlano.CancellationPending = True) Then e.Cancel = True End Sub Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles atividadeSegundoPlano.ProgressChanged ' ----- A tarefa em segundo plano atualiza a barra de progresso pgbProgressoOperacao.Value = e.ProgressPercentage End Sub Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles atividadeSegundoPlano.RunWorkerCompleted ' ----- Termina. If (e.Cancelled = True) Then lblEstadoOperacao.Text = "Operação Cancelada." Else lblEstadoOperacao.Text = "Operação Completa." End If pgbProgressoOperacao.Visible = False pgbProgressoOperacao.Value = 0 btnPararOperacao.Enabled = False btnIniciaOperacao.Enabled = True End Sub |
O código do relacionado com o evento Click do botão de comando Iniciar Operacão e Parar Operação:
Private
Sub btnIniciaOperacao_Click(ByVal sender As
System.Object, ByVal e As System.EventArgs) Handles
btnIniciaOperacao.Click ' ----- Inicia o processo em segundo plano btnIniciaOperacao.Enabled = False btnPararOperacao.Enabled = True lblEstadoOperacao.Text = "Em Progresso " pgbProgressoOperacao.Value = 0 pgbProgressoOperacao.Visible = True atividadeSegundoPlano.RunWorkerAsync() End Sub Private Sub btnPararOperacao_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPararOperacao.Click ' ----- Informa a thread para parar atividadeSegundoPlano.CancelAsync() End Sub |
O código da rotina executaTrabalho :
Private
Sub executaTrabalho(ByVal processoAtivo As
BackgroundWorker) ' ----- Realiza algum trabalho For contador As Integer = 1 To 10 ' ----- Verifica se vai sair agora If (processoAtivo.CancellationPending = True) Then _ Exit For ' ----- Aguarde por 2 segundos. Threading.Thread.Sleep(2000) ' ----- Informa a thread principal que temos feitos alterações processoAtivo.ReportProgress(contador * 10) Next contador End Sub |
Rode o programa e clique no botão - Iniciar Operação. A barra de progresso será atualizada conforme a operação em segundo plano estiver sendo executada .
Você pode interromper a atividade de segundo plano clicando no botão - Parar Operação.
Os processos executados no Windows possuem a opção de dividir o seu trabalho entre threads de execução separadas dentro destes processos. Por padrão, o processo VB, inclui somente uma thread única de processamento. Porém você pode iniciar uma ou mais threads ativas em segundo plano para realizar alguma atividade à parte do fluxo da aplicação principal.
A .NET Framework inclui o suporte a threads (linhas de execução) através do namespace System.Threading e especificamente através da classe Thread. O uso da classe Thread é simples, mas para poder interagir com outras threads você deverá criar o seu próprio código.
O controle BackgroundWorker é parte do namespace System.ComponentModel, e implementa um conjunto destas interações para você. Para usar este controle você pode simplesmente incluir o controle a partir da barra de ferramentas do VB 2005 ou você também pode usá-lo como uma classe declarando a palavra-chave WithEvents :
Private WithEvents BackgroundActivity As System.ComponentModel.BackgroundWorker
Quando você estiver pronto para inciar uma tarefa em segundo plano, chame o método RunWorkerAsync da classe BackgroundWorker. Isto irá disparar o evento DoWork e neste evento chame o método que irá realizar o trabalho em segundo plano. O código acima passa uma instância BackgroundWorker para o método de trabalho. Você não precisa necessariamente passar esta informação mas fazendo isto fica mais fácil a comunicação de retorno para thread primária se ela existir.
Se você quiser que a thread ativa notifique o seu progresso defina a propriedade WorkerReportsProgress como True, então monitore o evento ProgressChanged do controle e chame o método ReportProgress enviando as notificações para a thread principal.
Esta comunicação
funciona dos dois lados. Definindo a propriedade WorkerSupportsCancellation
do controle como True você permite que a thread primária efetue
uma requisição de cancelamento do trabalho via chamada do
método CancelAsycn() e isto define a
propriedade CancellationPending do controle como
vista pela thread de trabalho.
O processamento em segundo plano fica muito simples usando
threads , mas as interações entre as threads pode lhe causar
muitas dores de cabeça. O problema é que se duas threads
desejam atualizar a mesma instância do objeto não há garantia
de que elas irão fazer isto em um ordem específica .
Vejamos um exemplo que demonstra esta ocorrência:
Considere uma classe hipotética constituída de 3 membros sendo que a atualização destes membros ocorre sobre múltiplos comandos:
Private
instancia As ClasseExemplo Private Sub atualizaInstancia(ByVal escalar As Integer) instancia.Membro1 = 10 * escalar instancia.Membro2 = 20 * escalar instancia.Membro3 = 30 * escalar End Sub |
Veja o que pode ocorrer quando duas threads diferentes chamam o
método atualizaInstancia() ao mesmo tempo (assumindo que eles
estão compartilhando a variável instancia. Devido a forma que
as threads trabalham é possível que a chamada possa ocorrer de
forma intercalada o que implicaria na corrupção dos dados .
Suponha que a thread #1 chame atualizaInstancia(2) e a thread #2
chame atualizaInstancia(3). É possível que o comando dentro de
atualizaInstancia possa ser chamada na seguinte ordem:
instancia.Membro1
= 10 * 2 ' chamada pela Thread #1
instancia.Membro1 = 10 * 3 '
chamada pela Thread #2
instancia.Membro2 = 20 * 3 '
chamada pela Thread #2
instancia.Membro2 = 20 * 2 '
chamada pela Thread #1
instancia.Membro3 = 30 * 2 '
chamada pela Thread #1
instancia.Membro3 = 30 * 3 '
chamada pela Thread #2
Após a execução
acima , Membro1 e Membro3 terão sidos atualizados com com base na
chamada da Thread #2 ao passo que Membro2 terá sido atualizada
com base na Thread #1
Para prevenir este problema temos a disposição o comando SyncLock que atual como um protetor no bloco de código. Usando SyncLock para o caso acima você precisa criar um objeto comum e usar o mecanismo de bloqueio, vejamos como ficaria o código:
Private
instancia As ClasseExemplo Private bloquearObjeto as New Object Private Sub atualizaInstancia(ByVal escalar As Integer) SyncLock bloquearObjeto instancia.Membro1 = 10 * escalar instancia.Membro2 = 20 * escalar instancia.Membro3 = 30 * escalar End SyncLock End Sub |
Conforme cada thread chame o método atualizaInstancia, o comando SyncLock tenta bloquear exclusivamente a instância bloquearObjeto. Somente quando esta operação ocorre com sucesso o a thread processa o bloco de código.
O exemplo acima não têm utilidade prática alguma a não ser mostrar a forma básica de usar os eventos do controle BackgroundWorker.
Vamos a algo mais interessante...
Nota: Alerta sobre a utilização de threads em aplicações
Windows Forms:
Ocorre que os formulários Windows são baseados em um modelo STA - single-threaded apartment; eles podem ser criados em qualquer thread, mas depois que eles foram criados não podem mudar para uma thread diferente, e os métodos dos formulários Windows também não podem ser acessados em outra thread distinta daquela na qual eles foram criados. Isto significa que todos os métodos dos formulários (e os controles criados na mesma thread) devem ser executados na thread que criou o formulário ou controle do formulário. Se você tentar chamar um método de um formulário windows em uma thread distinta, uma exceção será disparada, e, dependendo de como você implementou o tratamento de exceção no seu código a aplicação pode encerrar. Se você não estiver efetuando um tratamento de exceção será exibida a seguinte mensagem: An unhandled exception of type 'System.ArgumentException' occurred in system.windows.forms.dll |
Vamos usar o controle BackgroundWorker para contornar este problema.
Crie um novo projeto no VB2005 do tipo Windows Application com o nome de testeAssincrono e a partir da toolbox arraste para o formulário form1.vb o controle BackgroundWorker; a seguir inclua um controle dataGridView e um controle Button conforme o leiaute abaixo:
Declare o seguinte namespace no formulário:
Imports System.Data.sqlclient
a seguir defina a seguinte variável objeto:
Dim dt As DataTable
Agora no evento DoWork do controle BackgroundWorker inclua o seguinte código:
Private
Sub BackgroundWorker1_DoWork(ByVal sender As
System.Object, ByVal e As
System.ComponentModel.DoWorkEventArgs) Handles
BackgroundWorker1.DoWork Try Using cn As New SqlConnection("Data Source=.\SQLEXPRESS;AttachDbFilename=C:\dados\Cadastro.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True") Dim cmd As SqlCommand = New SqlCommand("SELECT * FROM Clientes", cn) dt = New DataTable("Clientes") Dim da As SqlDataAdapter = New SqlDataAdapter(cmd) da.Fill(dt) End Using SyncLock DataGridView1 DataGridView1.DataSource = dt End SyncLock Catch ex As Exception MsgBox(ex.Message) End Try End Sub |
e no evento Click do botão de comando inclua o seguinte comando:
Private
Sub btnCarregaDados_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles
btnCarregaDados.Click BackgroundWorker1.RunWorkerAsync() End Sub |
Tente rodar o projeto e você obterá uma mensagem de erro conforme abaixo:
Não há dúvidas ... Estamos acessando um controle Windows Form a partir de uma thread diferente daquela da qual o controle foi criado.
Para corrigir o problema temos que tratar outro evento : RunWorkerCompleted. Fazendo isto , a chamada de volta já retornou e o acesso ao controle DataGridView ou a qualquer outro controle esta protegida.
Comente ou remova o código do que carregava o datagridView do evento DoWork:
SyncLock
DataGridView1
DataGridView1.DataSource = dt
End SyncLock
Vamos incluir o seguinte código no projeto:
Private
Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As
Object, ByVal e As
System.ComponentModel.RunWorkerCompletedEventArgs)
Handles BackgroundWorker1.RunWorkerCompleted CarregaDados() End Sub Private Sub CarregaDados() DataGridView1.DataSource = dt End Sub |
Agora ao rodar o projeto e clicar no botão de comando - Carregar Dados você verá o DataGridView ser carregado sem problema algum:
Pegue o código completo do projeto aqui: backgroundWorker.zip
Até o próximo artigo .NET
Veja os
Destaques e novidades do SUPER DVD Visual Basic
(sempre atualizado) : clique e confira !
Quer migrar para o VB .NET ?
Quer aprender C# ??
Quer aprender os conceitos da Programação Orientada a objetos ? Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ? |
Gostou ? Compartilhe no Facebook Compartilhe no Twitter
Referências: