0

What I'm trying to achieve is simple. I have a dynamic timer (one that can be changed by the user) which calls on background worker to go and fetch the user's external IP address. The combination of Timer and BackgroundWorker is causing some problems. Here's the code:

namespace IPdevices
{
    /// <summary>
    /// Interaction logic for Main.xaml
    /// </summary>
    public partial class Main : Window
    {

        private readonly BackgroundWorker worker;

        private IPret iprep;

        private Timer timer;


        public Main(Client client)
        {

            InitializeComponent();

            iprep = new IPret();

            startClock();

            worker  = new BackgroundWorker();
            worker.DoWork += worker_DoWork;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;
            worker.WorkerReportsProgress = true;
            worker.ProgressChanged += worker_ProgressChanged;

        }

        private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            ipAdd.Content = e.UserState;
        }

        private void startClock()
        {
            timer = new Timer();
            timer.Interval = 2000;
            timer.Elapsed += new ElapsedEventHandler(clockTimer_Tick);
            timer.Start();
        }

        private void clockTimer_Tick(object sender, ElapsedEventArgs e)
        {
            timer.Stop();
            worker.RunWorkerAsync();
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            Console.WriteLine("Checking ip");
            iprep.refresh();
            worker.ReportProgress(0, iprep.getExternalIp());
            Console.WriteLine("Found ip");
        }

        private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            timer.Start();
        }

    }
}

Essentially, once the timer fires, I wish to fetch the ip address and output on a label in the application. However, I get an exception in the ProgressChanged method saying that it can't be changed because another thread owns it. Which thread is that? Is it the iprep that is owned by another thread? In fact, RunWorkerCompleted never gets fired. I'm having trouble understanding which threads own what and how objects are locked...Any insight would be appreciated.

Dimitri
  • 1,906
  • 3
  • 25
  • 49
  • There is no issue with `iprep.refresh();` or `iprep.getExternalIp()`? – Uueerdo Aug 12 '16 at 18:42
  • @Uueerdo no. It runs nicely by itself. Though the only problem I foresee is that the refresh might take some time to fetch the ip address of the client. But other than that, it runs every time when not in a thread. – Dimitri Aug 12 '16 at 18:43
  • Have you tried copying the `e.UserState` object and using the copy instead? Perhaps it is owned by the BackgroundWorker; I don't think I've ever tried binding to or altering such an object. – Uueerdo Aug 12 '16 at 18:50
  • @Uueerdo You're right. Thanks for the heads up! – 15ee8f99-57ff-4f92-890c-b56153 Aug 12 '16 at 18:52
  • @Uueerdo what would copying do exactly in this case? – Dimitri Aug 12 '16 at 18:53
  • It would prevent the WPF framework from trying to work with an object from potentially another thread; the copy would be made on the appropriate thread. (As I commented in Ed's former answer, BackgroundWorker does some magic so you don't have to worry about using dispatchers in your event handler for your own methods/members/etc; there may be some complexities to how the e.UserState property is implemented that are not encountered in normal use.) _When I expect a "final" object back from a backgroundworker I typically use e.Result and retrieve it on the RunWorkerCompleted event._ – Uueerdo Aug 12 '16 at 18:58
  • @Uueerdo let me look into this. I'll get back to you. – Dimitri Aug 12 '16 at 19:04
  • Where is the sense in having a Timer that runs on a separate thread, and a BackgroundWorker, that runs on yet another separate thread. Seems far too complicated. What are you *actually* trying to achieve? – Clemens Aug 12 '16 at 19:08
  • @Clemens this is what first came to mind. I wanted to create a timer that will be displayed as a countdown for the user which would show the next time the ip address will be retrieved and displayed. I used backgroundworker to achieve this fetching process since I was having trouble originally with UI freezing... – Dimitri Aug 12 '16 at 19:10
  • @Uueerdo I've removed everything and literally tried setting the UI control to an empty string without using UserState. In fact, in the DoWork method, I pass an empty string in the ReportProgress. I still get the error that I cannot access the UI control. I even tried accessing the control in the RunWorkerCompleted... – Dimitri Aug 12 '16 at 19:21
  • @Uueerdo I found [this](http://stackoverflow.com/questions/10795948/the-calling-thread-cannot-access-this-object-because-a-different-thread-owns-it), though like I told ED before, I don't remember using `BeginInvoke` in WinForms...In fact, I found some old projects where I would be able to access UI elements from within the `ProgressChanged` method. Perhaps it's been too long... – Dimitri Aug 12 '16 at 19:25
  • Is retrieving the IP address an operation that would block the calling thread for a longer time? If not you could simply do it in the Timer's Elapsed handler. – Clemens Aug 12 '16 at 19:26
  • Which Timer are you using System.Threading or System.Timers? (The problem doesn't seem to occur without the timer.) – Uueerdo Aug 12 '16 at 19:35
  • @Uueerdo using System.Timers; – Dimitri Aug 12 '16 at 19:36
  • Also, not sure if it matters, but you are starting your timer before the backgroundworker is initialized. – Uueerdo Aug 12 '16 at 19:37
  • @Uueerdo noticed that also. It's fixed, though the problem remained. – Dimitri Aug 12 '16 at 19:38
  • My guess is that the timer's tick calls the worker's run on a different thread, and the worker is reporting its progress on the calling (timer's) thread. Maybe the timer needs an invoke to make sure the worker is called from, and reports to, the UI thread. – Uueerdo Aug 12 '16 at 19:41

3 Answers3

2

This appears to fix it in my test of it

    private void clockTimer_Tick(object sender, ElapsedEventArgs e)
    {
        timer.Stop();
        Action a = () =>
        {
            worker.RunWorkerAsync();
        };
        Application.Current.Dispatcher.BeginInvoke(a);
    }

Also, I'll note this is consistent behavior for Timer in WPF (I hadn't used it in WPF before); trying ipAdd.Content = "Tick"; in the clockTimer_Tick causes the same error. System.Timers.Timer's tick event does not happen on the UI thread.

Uueerdo
  • 15,723
  • 1
  • 16
  • 21
  • I'll be... this worked. I'm not entirely sure how you figured this out and what the semantics are... – Dimitri Aug 12 '16 at 19:49
  • `Application.Current.Dispatcher.BeginInvoke` invokes on the UI thread, guaranteeing the worker is started the same way it would be (from the same thread) as though you had clicked a button or triggered it through some other user action. – Uueerdo Aug 12 '16 at 19:51
  • It appears Timers have their own tick threads; that is what caused the issue. The timer spawned the worker off of it's thread, not the UI thread. So the events from the worker came back on the timer's thread, which does not "have access" to the UI thread that "owns" the timer itself. It was sort of as if you were trying to execute the progresschanged code in the tick event. – Uueerdo Aug 12 '16 at 20:02
  • Gotcha! Thanks a million for the help. Cheers! – Dimitri Aug 12 '16 at 20:04
  • 2
    "It appears Timers have their own tick threads" That's well documented. Use a DispatcherTimer if you want the Tick handler to be executed in the UI thread. BeginInvoke would not be necessary then. So just replace Timer by DispatcherTimer. – Clemens Aug 12 '16 at 20:20
  • @Dimitri Nexts step would be to replace the BackgroundWorker by an async Task that is awaited in the Tick handler. – Clemens Aug 12 '16 at 20:30
  • @Clemens I saw your comment on Async and Iprogress. Would need to find the documentation to this; completely unfamiliar territory, but would be great if I can replace BackgroundWorker. – Dimitri Aug 12 '16 at 20:33
  • @Clemens will be testing it out in a bit! Thank you! – Dimitri Aug 12 '16 at 20:48
1

ipAdd is an UI element if I am not mistaken. If it is then the problem lies on cross threading.

What happened is that Background worker is going to be running on a different thread than the UI thread. If you want to modify UI element's property you need to do it on the UI thread. One option is to use Dispatcher.Invoke but since you are using WPF, there is a better way to do it.

Do a search about MVVM design patter and move the background code into View Model. Then you could do something like

string _XXContent 
public string XXContent 
{
    get
    {
        return _XXContent;
    }
    set
    {
        _XXContent = value;
        OnPropertyChanged("XXContent");
    }
}


 private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
 {
     XXContent = e.UserState;
 }

xaml :

<TextBox Content={Binding XXContent}/>

Edit: If you are on c# 5 then you should look into async/IProgress as well an get rid of Background worker.

Steve
  • 11,696
  • 7
  • 43
  • 81
  • I'll be looking into this as well. Thanks for the input. Working with WPF has been a roller-coaster. – Dimitri Aug 12 '16 at 19:40
  • The only issue I have with this is, your pushing MVVM on the person but not recommending ASYNC and IProgress. I would really suggest stepping away from the BackgroundWorker and just turn your methods into async methods and pass an delegate with IProgress. It will look cleaner in your view model and make it easier to debug and write unit tests for. – Kevin B Burns Aug 12 '16 at 19:41
1

Replace all your code by the few lines shown below. The Tick handler is executed in the UI thread. Still it asynchronously runs a background operation and does not block the UI thread.

private void StartClock()
{
    var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
    timer.Tick += async (o, e) => await GetIP();
    timer.Start();
}

private async Task GetIP()
{
    Debug.WriteLine("Checking ip");

    await Task.Run(() =>
    {
        // Get the IP asynchronously here
    });

    Debug.WriteLine("Found ip");

    // Update the UI here
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • This worked like a charm!! This reminds me a lot of my operating systems class when we dealt with semaphores, locks and monitors. – Dimitri Aug 12 '16 at 20:58