views:

365

answers:

4

I've got a C# program that talks to an instrument (spectrum analyzer) over a network. I need to be able to change a large number of parameters in the instrument and read them back into my program. I want to use backgroundworker to do the actual talking to the instrument so that UI performance doesn't suffer.

The way this works is - 1) send command to the instrument with new parameter value, 2) read parameter back from the instrument so I can see what actually happened (for example, I try to set the center frequency above the max that the instrument will handle and it tells me what it will actually handle), and 3) update a program variable with the actual value received from the instrument.

Because there are quite a few parameters to be updated I'd like to use a generic routine. The part I can't seem to get my brain around is updating the variable in my code with what comes back from the instrument via backgroundworker. If I used a separate RunWorkerCompleted event for each parameter I could hardwire the update directly to the variable. I'd like to come up with a way of using a single routine that's capable of updating any of the variables. All I can come up with is passing a reference number (different for each parameter) and using a switch statement in the RunWorkerCompleted handler to direct the result. There has to be a better way.

A: 

[Edit - look back at update history to see previous answer. Talk about not being able to see the wood for the trees]

Is there any reason that, rather than passing a reference number to the Background Worker, you can't pass the ID of the label that should be updated with any value passed back?

So the UI adds an item in the work queue containing:

  • Variable to change
  • Attempted change
  • UI ID

and the BackgroundWorker triggers an event with EventArgs containing

  • Attempted change
  • Actual value after attempt
  • UI ID
  • Error Message (null if successful)

which is all the information you need to update your UI without a switch or multiple event args and without your Background Worker ever being aware of UI detail.

pdr
Okay - thanks for the help. I was originally looking for something like reference parameters, but couldn't see any way to make the connections. Thanks again.
Bruce
That's a very interesting idea! I'll look into it. Thanks again - Bruce
Bruce
A: 

UPDATE: I've added the DoWork event handler to make it clearer how data goes to and from the worker. UPDATE2: It occurred to me that my example may not be thread-safe because I'm updating the dictionary in the RumWorkerCompleted event handler. This could be bad if you were executing multiple workers concurrently. Added appropriate comment.

First, do you want to send parameters to the instrument asynchronously with regards to each other (several parallel update threads)? If not, your problem is simple because you can just have a local class member which holds a reference to the parameter you're currently updating in the background.

But, assuming you do want to do parallel updates: Your second idea seems basically sound, but could be refined. How about keeping all your variables in a dictionary? That way, when you call RunWorkerAsync() you can pass it a KeyValuePair, where the value is an Object into which the BackgroundWorker will store the result. Once the result is obtained, the BackgroundWorker returns it to you via either the ProgressChanged event or the RunWorkerCompleted event, both of which have a field for user state.

Your event handler then needs only update your dictionary using the returned key and value.

e.g. Something like:

// this holds all your parameters; assume you've populated it
Dictionary<string, object> InstrumentParameters = new Dictionary<string, object>();

// call this for each parameter update you want to perform
void UpdateParameter(string parameterName)
{
    BackgroundWorker bgw = new BackgroundWorker();
    bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw_RunWorkerCompleted);
    bgw.DoWork += new DoWorkEventHandler(bgw_DoWork);

    // note, this is bad without a deep clone (*)
    KeyValuePair<string, object> pair = InstrumentParameters.Single(p => p.Key == parameterName);                    
    bgw.RunWorkerAsync(pair); 
}

void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    // get the parameter to be updated, and value
    KeyValuePair<string, object> argument = (KeyValuePair<string, object>)(e.Argument);
    // if you want to do anything else, like send progress info back, you'll need a reference to the worker:
    //BackgroundWorker bgw = sender as BackgroundWorker;

    // communicate with your device here...

    // set the return value. (12345 in this example)
    KeyValuePair<string, object> result = new KeyValuePair<string, object>(argument.Key, 12345);
    e.Result = pair;  // this will be passed to your RunWorkerCompleted handler.
}

void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // note that this is not thread safe. you should acquire a lock while updating InstrumentParameters
    KeyValuePair<string, object> result;
    result = (KeyValuePair<string, object>)(e.Result);
    // do whatever processing you need here...
    // e.g. update the value in the dictionary:
    InstrumentParameters[result.Key] = result.Value;                
}

One problem I see with this, as mentioned (*) in the comments, is that you are presumably getting a shallow copy of the KeyValuePair out of the dictionary here. This means the value (since I chose to use Object) is just a reference to the actual value in the dictionary, and you don't want to shoot that off to your background thread. So you could either use a value type here, or add some logic to make a real copy of that object, to pass to the worker.

Charles
I'm only updating one parameter at a time (the spec an will choke if you try to change more than one item at a time). The question I have about a wrapper class is in terms of timing. The wrapper class sets up the call to RunWorkerAsync and then returns. How do I connect RunWOrkerCompleted so I can access a reference passed to the wrapper? (I hope that's not a seriously stupid question.) Thanks again for your help.
Bruce
I've answered that in my example, but I omitted the front end of the call - the DoWork event handler. I will edit my answer to include that part too. RunWorkerCompletedEventArgs.Result is a field intended to be used for that purpose. So in your DoWork event handler, you communicate with the instrument, and then whatever value you want to return, you put it in the Result field of the DoWorkEventArgs parameter (this will then be passed back as the RunWorkerCompletedEventArgs parameter of the RunWorkerCompleted event, when DoWork() exits).
Charles
A: 

How about something like this?

[TestFixture]
public class BGWorkerTest
{
    string output1;
    string output2;

    [Test]
    public void DoTest()
    {
        var backgroundWorker = new BackgroundWorker();
        backgroundWorker.DoWork += (sender, args) =>
                                   {
                                       output1 = DoThing1();
                                       output2 = DoThing2();
                                   };
        backgroundWorker.RunWorkerAsync();
        //Wait for BG to finish
        Thread.Sleep(3000);
        Assert.AreEqual("Thing1",output1);
        Assert.AreEqual("Thing2",output2);
    }

    public string DoThing1()
    {
        Thread.Sleep(1000);
        return "Thing1";
    }
    public string DoThing2()
    {
        Thread.Sleep(1000);
        return "Thing2";
    }
}
Simon
would that not negate the point of using a background worker?
David Neale
Not sure what you mean? If you mean the sleep I only put it there to illustrate that after a given time the methods have run and the correct values are set.
Simon
+1  A: 

I think what I would do is pass a list of parameters, values, and delegates to the BackgroundWorker. That way you can write the assign-back code "synchronously" but have execution deferred until the values are actually retrieved.

Start with a "request" class that looks something like this:

class ParameterUpdate
{
    public ParameterUpdate(string name, string value, Action<string> callback)
    {
        this.Name = name;
        this.Value = value;
        this.Callback = callback;
    }

    public string Name { get; private set; }
    public string Value { get; set; }
    public Action<string> Callback { get; private set; }
}

Then write your async code to use this:

private void bwUpdateParameters_DoWork(object sender, DoWorkEventArgs e)
{
    var updates = (IEnumerable<ParameterUpdate>)e.Argument;
    foreach (var update in updates)
    {
        WriteDeviceParameter(update.Name, update.Value);
        update.Value = ReadDeviceParameter(update.Name);
    }
    e.Result = updates;
}

private void bwUpdateParameters_RunWorkerCompleted(object sender,
    RunWorkerCompletedEventArgs e)
{
    var updates = (IEnumerable<ParameterUpdate>)e.Argument;
    foreach (var update in updates)
    {
        if (update.Callback != null)
        {
            update.Callback(update.Value);
        }
    }
}

Here's how you would kick off the update. Let's say you've got a bunch of member fields that you want to update with the actual values of the parameters that were used:

// Members of the Form/Control class
private string bandwidth;
private string inputAttenuation;
private string averaging;

// Later on, in your "update" method
var updates = new List<ParameterUpdate>
{
    new ParameterUpdate("Bandwidth", "3000", v => bandwidth = v),
    new ParameterUpdate("InputAttenuation", "10", v => inputAttenuation = v),
    new ParameterUpdate("Averaging", "Logarithmic", v => averaging = v)
};
bwUpdateParameters.RunWorkerAsync(updates);

That's all you have to do. All of the actual work is done in the background, but you're writing simple variable-assignment statements as if they were in the foreground. The code is short, simple, and completely thread-safe because the actual assignments are executed in the RunWorkerCompleted event.

If you need to do more than this, such as update controls in addition to variables, it's very simple, you can put anything you want for the callback, i.e.:

new ParameterUpdate("Bandwidth", "3000", v =>
{
    bandwidth = v;
    txtBandwidth.Text = v;
})

Again, this will work because it's not actually getting executed until the work is completed.

Aaronaught