Josh Bibb
Josh Bibb

Reputation: 477

Need help setting up threads/background worker in GUI

I'm using C# and Winforms in Visual Studio 2010

I have a program with which I am trying to read output through a serial port and print it to the screen. It originally started as a Console program but has now evolved to where we would like to have the output be in a field on a form. I have the code that parses out the output I'm looking for off the serial port written and working, I just need to change the Console.WriteLine to label.text = "";, basically. I have merged the function that listens to the serial port into the GUI code so everything is in the same file.

I'm getting hung up on how to get the function to write to the label, though. It is STATIC so I cant just say 'label.text ='. I tried creating a new form object inside the function to use, and that allowed me to access the control on the form, but doesnt update the form I see at runtime (I'm guessing because I've created a new instance of the form rather than accessed the existing instance?)

I need to have the serial listener run at the same time as the GUI as well, so the GUI label will update with the results it gets from running the function in close to real-time, so Ive tried to set it up to be threaded, with the GUI being one thread that is started by main() and the serial listener being another thread which is started when i click the button to start it. However, I run into the same issue with not being able to access the label in the serial listener thread because it has to be static to be initialized using system.threading.

I'm thinking maybe I need to use a background worker for the serial listener but I have absolutely zero experience with those. Would a background worker be able to update the label on the GUI in real time?

I cant post specific code but heres the general idea:

Main() starts GUIthread

GUI has button to start serial listener OnClick button starts ListenerThread ListenerThread outputs to console, want to output to a form label instead

Cant access GUI.Label because Listener is static out of necessity to be threaded Creating new GUI instance inside Listener allows me to call the controls for that instance, but they dont update the GUI at runtime

have ensured label is public.

Upvotes: 0

Views: 1205

Answers (4)

Servy
Servy

Reputation: 203813

The BackgroundWorker class was essentially made just for this.

Just have the DoWork method do your actual work, and ensure that ReportProgess is called while working as needed. You can pass any data as a string (or whatever else, if you want) and then use that value in the ProgressChanged event handler, which the form can handle to update it's UI.

Note that the BackgroundWorker will automatically ensure that the ProgressChanged and RunWorkerCompleted events run in the UI thread, so you don't need to bother with that.

Here's a sample worker:

public class MyWorker//TODO give better name
{
    public void DoWork(BackgroundWorker worker)//TODO give better name
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(1000);//to mimic real work
            worker.ReportProgress(0, i.ToString());
        }
    }
}

And here's an example of configuring the background worker. Here I use lambdas both because it's convenient to be able to close over variables (i.e. use variables across each of these anonymous methods) but if you wanted to you could refactor each of the event handlers out into methods.

private void button1_Click(object sender, EventArgs e)
{
    var bgw = new BackgroundWorker();
    MyWorker worker = new MyWorker();

    bgw.WorkerReportsProgress = true;
    bgw.DoWork += (s, args) => { worker.DoWork(bgw); };
    bgw.ProgressChanged += (s, data) =>
    {
        label1.Text = data.UserState.ToString();
    };
    bgw.RunWorkerCompleted += (s, args) =>
    {
        label1.Text = "All Done!";
    };

    bgw.RunWorkerAsync();//actually start the worker
}

Note here that none of the controls in the form are public, none of them are static, and I'm not passing any references to my form outside of the class. It's considered best form each Form to be responsible for updating it's own Controls. You shouldn't be allowing anyone else to directly access them. Rather than allowing some other worker class to directly access the label or modify it's text, what's happening is that the worker is simply telling the form, "Hey, I've got some data, you can go update yourself accordingly based on these values." It is then the form that is responsible for updating itself. events are what you use to allow these workers, or other types of child elements (such as other forms you create, for example) to inform the "parent" form that it needs to update itself.

Upvotes: 2

Mike
Mike

Reputation: 974

Going on what you said about a static listener method and that it used to be a console application, I think a relatively minor modification might be the following:

class Program
{
    static void Main(string[] args)
    {
        // Create a main window GUI
        Form1 form1 = new Form1();

        // Create a thread to listen concurrently to the GUI thread
        Thread listenerThread = new Thread(new ParameterizedThreadStart(Listener));
        listenerThread.IsBackground = true;
        listenerThread.Start(form1);

        // Run the form
        System.Windows.Forms.Application.Run(form1);
    }

    static void Listener(object formObject)
    {
        Form1 form = (Form1)formObject;

        // Do whatever we need to do
        while (true)
        {
            Thread.Sleep(1000);
            form.AddLineToTextBox("Hello");
        }
    }
}

In this case, Form1 is obviously the form class, and Listener is the listening method. The key here is that I'm passing the form object as an argument to the Listen method (via Thread.Start), so that the listener can access the non-static members of the GUI. Note that I've defined Form1.AddLineToTextBox as:

public void AddLineToTextBox(string line)
{
    if (textBox1.InvokeRequired)
        textBox1.Invoke(new Action(() => { textBox1.Text += line + Environment.NewLine; }));
    else
        textBox1.Text += line + Environment.NewLine;
}

Note especially that since now the Listener method is running in a separate thread, you need to use the Invoke method on the GUI control to make a change. I've used a lambda expression here, but if you're targeting an earlier version of .net you could use a full method just as easily. Note that my textBox1 is a TextBox with Multiline set to true and ReadOnly set to false (to be similar to a label).

An alternative architecture which may require more work but would probably be more elegant would be to do the opposite dependence relationship: you create the form with a reference to a Listener object. The listener will then raise events which the GUI would be subscribed to in order to update its display.

Upvotes: 0

fabricio
fabricio

Reputation: 1393

I think you can use a background worker, and they are really easy to use.

In order to use a BackgroundWorker, you'll have to implement at least two events:

backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

there you read your input. It's triggered calling backgroundWorker1.RunWorkerAsync(...)

backgroundWorker1_ProgressChanged(....)

there you update your label. Maybe you'll have to create a delegate to update it.

you can also implement:

backgroundWorker1_RunWorkerCompleted(....)

to let you know when it stop...

Upvotes: 0

Richard Schneider
Richard Schneider

Reputation: 35464

To write to any windows control, you must be on the UI thread. If you have a serial listener running on a different thread, then you need to switch threads before changing the windows control. The BeginInvoke can be handy, http://msdn.microsoft.com/en-us/library/system.windows.forms.control.begininvoke.aspx.

What I would do, is add a Action to the serial listener that is called whenever the listener wants to display something. And then this Action would call BeginInvoke.

Something like:

static class SerialListner
{
    public Action<string> SomethingToDisplay;

    void GotSomethingToDisplay(string s)
    {
        SomethingToDisplay(s);
}

And then somewhere in your windows form

SerialListern.SomethingToDisplay = (s) => 
   label.BeginInvoke((Action) () => label.Text = s);

Upvotes: 0

Related Questions