3

I have a button that when pressed gets the status of various windows services on remote PCs. I would like to refresh this button automatically every minute so that the latest status of the services are always shown.

I have tried to set a timer but I keep getting the error "Cross-thread operation not valid: Control 'btnRefreshServices' accessed from a thread other than the thread it was created on"

Any help appreciated

    private void btnRefreshServices_Click(
        object sender,
        EventArgs eventArgs)
    {
        this.btnRefreshServices.Enabled = false;

        // Setting up progress bar in a separate thread to update the progress bar
        // This is necessary so that the dialog doesn't freeze while the progress bar is reporting its progress
        this.prgbServiceStatus.Minimum = 1;
        this.prgbServiceStatus.Maximum = 11;
        this.prgbServiceStatus.Step = 1;
        this.prgbServiceStatus.Value = 1;
        this.prgbServiceStatus.Increment(1);
        this.prgbServiceStatus.PerformStep();

        var _backgroundWorker = new BackgroundWorker();
        _backgroundWorker.ProgressChanged += ProgressChanged;
        _backgroundWorker.DoWork += DoWork;
        _backgroundWorker.WorkerReportsProgress = true;
        _backgroundWorker.RunWorkerAsync();
        _backgroundWorker.RunWorkerCompleted += new  RunWorkerCompletedEventHandler(RunWorkerCompleted);
    }

    private void DoWork(
        object sender,
        DoWorkEventArgs doWorkEventArgs)
    {
        // Get the current status of each Windows service and reflect the progress in the progress bar
        // NOTE: If you add a new service, divide the number of services by 100 and update each call to report progress
        ((BackgroundWorker)sender).ReportProgress(15);

        CurrentStatus(
            this.grdResults,
            ServerA,
            ServiceName,
            RowIndexA);
        ((BackgroundWorker)sender).ReportProgress(25);

        CurrentStatus(
            this.grdResults,
            ServerB,
            ServiceNameB,
            RowIndexB);
        ((BackgroundWorker)sender).ReportProgress(35);

}

I was using something like this code for the timer

    Timer myTimer = new Timer();
    myTimer.Elapsed += new ElapsedEventHandler(DisplayTimeEvent);
    myTimer.Interval = 1000; // 1000 ms is one second
    myTimer.Start();

    public static void DisplayTimeEvent(object source, ElapsedEventArgs e)
    {
       // code here will run every second
    }

Using Emile Pels code I was able to fix my problem.

    public frmServicesManager()
    {
        InitializeComponent();

        // The interval in milliseconds (1000 ms = 1 second)
        const double interval = 5000.0;

        // Create a new timer
        new System.Timers.Timer()
            {
                Enabled = true,
                Interval = interval
            }.Elapsed += TimerCallback;
    }

    private void TimerCallback(
        object sender, 
        ElapsedEventArgs elapsedEventArgs)
    {
        // SignalTime is now of type DateTime and contains the value indicating when the timer's Elapsed event was raised
        var _signalTime = elapsedEventArgs.SignalTime;

        // Create a new Action
        var _setButtonClick = new Action<DateTime>(dateTime => this.btnRefreshServices.PerformClick());

        // Check if we can access the control from this thread
        if (this.btnRefreshServices.InvokeRequired)
        {
            // We can't access the label from this thread,so we'll call invoke so it is executed from the thread the it was created on
            this.btnRefreshServices.Invoke(_setButtonClick, _signalTime);
        }
    }

2 Answers2

3

Use a System.Windows.Forms.Timer, or set the button's property from another thread like this:

myButton.Invoke(new Action<string>((text) => myButton.Text = text), "New button text");

Edit: here's an example with more explanation.

The reason you're getting that error is that you're trying to access controls created on an other thread, which won't work. You will have to call the control's Invoke() method; that executes the delegate you pass it on the thread the control was created on. One of the delegates you can use is Action, as I demonstrate later in this post.

For the following example I used a System.Timers.Timer, and created a new Winforms project and added only a Label to it. Its name is timeLabel.

I placed this code inside my form's constructor:

//The interval in milliseconds (1000 ms = 1 second)
const double interval = 1000.0;

//Create a new timer
new System.Timers.Timer()
{
    Enabled = true, //Start it right away
    Interval = interval //Set the interval
}.Elapsed += TimerCallback; //Register a handler for the elapsed event

This creates a new timer and registers a callback to handle its Elapsed event, which is defined as follows:

private void TimerCallback(object sender, System.Timers.ElapsedEventArgs e)
{
    const string baseString = "The event was raised at {0}";

    //signalTime is now of type DateTime and contains the value 
    //indicating when the timer's Elapsed event was raised
    var signalTime = e.SignalTime;

    //Create a new Action - delegate - which takes a string argument
    var setLabelText = new Action<DateTime>(dt =>
    {
        //If the amount of seconds in the dt argument is an even number,
        //set the timeLabel's forecolor to red; else, make it green
        timeLabel.ForeColor = dt.Second % 2 == 0 ? Color.Red : Color.Green;

        //Format the baseString to display the time in dt
        timeLabel.Text = string.Format(baseString, dt.ToLongTimeString());
    });

    //Check if we can access the control from this thread
    if (timeLabel.InvokeRequired) {
        //We can't access the label from this thread,
        //so we'll call invoke so it is executed from
        //the thread the it was created on
        timeLabel.Invoke(setLabelText, signalTime);
    }
    else {
        //The label's text can be set from this thread,
        //we'll just call the delegate without Invoke()
        setLabelText(signalTime);
    }
}

This particular example changes a label's text to the current time every second, and if the amount of seconds is even, it makes the label's forecolor red: when it's odd, the color will be set to green. The program as it is may not seem very useful, but it demonstrates how you can access controls from an other thread; once you grasp this example, it should help you to expand it to fit your needs.

Emile Pels
  • 3,837
  • 1
  • 15
  • 23
0

It's not clear to me what all that BackgroundWorker code has to do with your question. It doesn't seem to have anything to do with the cross-thread issue, nor your periodic refresh of the button.

As far as what the question does seem to be about, you should be able to just use the correct Timer class (there are at least three in .NET), System.Windows.Forms.Timer. In that case, your code would look something like this:

System.Windows.Forms.Timer myTimer = new System.Windows.Forms.Timer();
myTimer.Tick += DisplayTimeEvent;
myTimer.Interval = 1000; // 1000 ms is one second
myTimer.Start();

public static void DisplayTimeEvent(object source, EventArgs e)
{
   // code here will run every second
}

The code above should be in a Winforms module, and so the System.Windows.Forms namespace should already be in scope, but I've fully-qualified the Timer class name above just for clarity.

Note also the event name is different: Tick instead of Elapsed. And the event handler signature is slightly different.

Using this Timer class instead of the one you were using, the Tick event handler will be invoked on the UI thread, avoiding any cross-thread issues altogether.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136