Há dias colocaram-me a seguinte questão, como passar parâmetros para uma Thread, e dentro dessa Thread como era possível escrever valores para um controlo (no caso uma TextBox), de modo a que a interface com o utilizador não ficasse bloqueada. A questão resume-se ao seguinte, como invocar assincronamente um método e permitir que esse método possa dar informações durante a sua execução.
A resposta é invariavelmente a mesma de sempre, há várias formas de o fazer, a forma como os parâmetros são passados para uma Thread varia inclusive com a versão da .Net Framework. Antes de escrever este post, fui ver o que se tinha escrito sobre a passagem de parâmetros para uma Thread, e encontrei 2 posts interessantes, um do Israel Aéce e outro do Luís Abreu. Em linhas gerais dizem tudo o que é necessário sobre a forma de passar parâmetros para uma Thread, mas em relação a esta questão há duas coisas que acho que falta dizer, a primeira prende-se com o facto de que tanto o delegate ParameterizedThreadStart (criar uma Thread) como o método QueueUserWorkItem (ThreadPool) passarem para o método um parâmetro do tipo object, isto vai provocar Boxing e Unboxing na passagem desse parâmetro, além disso essa chamada não é Type Safe.
Tendo em conta isto e como resposta a questão inicial, penso que a forma mais elegante e segura de o fazer é encapsulando a Thread, e utilizar um delegate, para que o método em execução possa ir fazendo output.
A classe que vai encapsular a Thread é a seguinte:
internal sealed class WorkerThread
{
private int loops;
private ThreadOutputCallback threadOutputCallback;
/// <summary>
/// Initializes a new instance of the <see cref="T:WorkerThread"/> class.
/// </summary>
/// <param name="outputCallback">The outputCallback.</param>
internal WorkerThread(ThreadOutputCallback outputCallback)
{
threadOutputCallback = outputCallback;
}
/// <summary>
/// Execute the specified lines.
/// </summary>
/// <param name="lines">The lines.</param>
internal void Loop(int lines)
{
//The the number of loops
loops = lines;
//Create the new thread object
Thread thread = new Thread(new ThreadStart(Run));
//Start the thread
thread.Start();
}
/// <summary>
/// Runs the specified argument.
/// </summary>
private void Run()
{
//Set the start value
int count = 0;
//Call the callback "loop" times
while (count++ < loops)
{
string message = string.Format("Loop {0} from {1}", count, loops);
WriteOutput(message);
}
WriteOutput("I'm done.");
}
/// <summary>
/// Writes the output.
/// </summary>
/// <param name="message">The message.</param>
private void WriteOutput(string message)
{
if (threadOutputCallback != null)
{
threadOutputCallback(message);
}
}
}
Não é a solução mais fácil ou imediata, mas deste modo evita-se fazer Boxing e Unboxing, e a forma como a Thread é iniciada (criar uma Thread ou utilizar a ThreadPool) fica encapsulada. Além disso a chamada ao método Loop é Type Safe, e recorrendo a um delegate, pode ser feito output da execução do método, este delegate é definido neste caso da seguinte forma:
internal delegate void ThreadOutputCallback(string message);
Isto resolve parte da questão, mas levanta uma outra, o output. O método para o qual aponta o delegate, está a ser executado numa Thread diferente, é necessário então garantir que a chamada ao método é executada na Thread correcta fazendo o seguinte:
private void ShowInfo(string message)
{
//Check if the method for the control is invoked in the correct thread
if (listBox.InvokeRequired)
{
//Create the new delegate to invoke the method in the correct thread
listBox.BeginInvoke(new ThreadOutputCallback(ShowInfo), new object[] {message});
}
else
{
//Update the text
listBox.Items.Add(message);
}
}
Ao criar o objecto é criado o delegate para o método de output, e a partir daqui pode ser invocado o método Loop:
WorkerThread worker = new WorkerThread(new ThreadOutputCallback(ShowInfo));
worker.Loop(lines);
Desta forma é possível invocar um método que vai ser executado numa outra Thread, e ao mesmo tempo permitir-lhe enviar informação durante a execução.
Como disse no início há várias maneiras de invocar métodos de forma assíncrona, esta é apenas uma alternativa, outra é por exemplo a utilização do delegate AsyncCallback, para invocar um método assincronamente, existe muita informação que podem consultar sobre Asynchronous Programming no MSDN.
Demo Project