0

I've written a piece of code that creates a lot of controls and layouts them on a canvas to visualize a tree. Now this code can take a lot of time, especially since it sometimes has to query an external service to see if there are more child node.

So I would like to show a progress bar while this code is executing. For other parts of my program I use a background worker that reports progress. However since I have to create controls that are later interact-able I don't see how to use a background worker or other threading solution here.

Since this is WPF, I also can't call Application.DoEvents(). So my question is, how can I create a lot of controls while still being able to periodically update the visual part of the GUI?

For my other code I use an Adorner that I layout over the busy piece of my app, I would prefer a solution where I can keep using that, I would also still prefer a solution using BackgroundWorker, but I'm pretty sure that is not possible.

I've looked at other SO topics, but I can't find a good answer so far

Creating controls in a non-UI thread

Creating a WinForm on the main thread using a backgroundworker

Edit:

According to this MSDN article http://msdn.microsoft.com/en-us/magazine/cc163328.aspx the BackgroundWorker should automatically invoke asynchronously on the UI thread if required, but this is not the behaviour I'm seeing, since I still see a cross thread exception.

Edit2: nvm, that's not totally true: BackgroundWorker still needs to call Invoke?

Edit3: After some more reading and some tips, this is the solution I've come to. Anybody got any tips/hints?

        // Events for reporting progress
        public event WorkStarted OnWorkStarted;
        public event WorkStatusChanged OnWorkStatusChanged;
        public event WorkCompleted OnWorkCompleted;


        private BackgroundWorker worker;
        private delegate void GuiThreadWork(object state);
        private PopulatableControlFactory factory = new PopulatableControlFactory();
        public Canvas canvas;

        public void PerformLayout(TreeNode node)
        {
            OnWorkStarted(this, "Testing");

            worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.DoWork += new DoWorkEventHandler(worker_DoWork);            
            worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
            worker.RunWorkerAsync(node);
        }

        private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            OnWorkCompleted(this);
        }

        private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            var workTuple = (Tuple<GuiThreadWork, TreeNode>)e.UserState;
            workTuple.First.Invoke(workTuple.Second); //Or begin invoke?

            if (OnWorkStatusChanged != null)
                OnWorkStatusChanged(this, e.ProgressPercentage);
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            TreeNode node = (TreeNode)e.Argument;            
            Thread.Sleep(1000);
            worker.ReportProgress(33, Tuple.New(Place(node), node));
            Thread.Sleep(1000);
            worker.ReportProgress(66, Tuple.New(Place(node.children[0]), node.children[0]));
            Thread.Sleep(1000);
            worker.ReportProgress(100, Tuple.New(Place(node.children[1]), node.children[1]));
        }

        private GuiThreadWork Place(TreeNode node)
        {            
            GuiThreadWork threadWork = delegate(object state)
            {
                PopulatableControl control = factory.GetControl((TreeNode)state);
                Canvas.SetLeft(control, 100);
                Canvas.SetTop(control, 100);
                canvas.Children.Add(control);
            };

            return threadWork;
        }   

In short: I use the progressChanged event of the background worker because this is always marshalled to the GUI thread. I pass it a tuple of a delegate and some state. This way I always create the control on the GUI thread and do all actions there, while still being flexible.

Community
  • 1
  • 1
Roy T.
  • 9,429
  • 2
  • 48
  • 70
  • 1
    I think a backgroundworker is perfectly viable for this, so long as you call invoke when adding your controls. – havardhu Sep 01 '11 at 14:28
  • Using a BackgroundWorker or separate thread and calling `Invoke()`/`BeginInvoke()` is the correct way to do this. If you're getting a threading exception, could you post your code? Also, are you sure you're creating your progress bar *before* launching the "working..." thread? – 3Dave Sep 01 '11 at 14:29
  • Hey havardhu and David Lively, thanks for the tips. I was aware of Invoke/BeginInvoke. But that would still leave me with that I create a control on the background thread, so I should go trough some length to work around this. What do you guys think about using the progressChanged event to execute a delegate given by the other thread to do these things? – Roy T. Sep 01 '11 at 14:33
  • whats the purpose of three 1 second thread sleep calls? – Valentin Kuzub Sep 01 '11 at 15:05
  • @Valentin Kuzub, simulating doing actual work, this is just test code to see if the idea works :) – Roy T. Sep 01 '11 at 15:08
  • how can you report progress if following is true: it sometimes has to query an external service to see if there are more child node? – Valentin Kuzub Sep 01 '11 at 15:10
  • generally I think your idea to pass big state object to ReportProgress method is flawed, just pass percentage in there, while doing actual work in doWork method. its more logical and clearer approach, work is done in doWork, progress reported in ReportProgress, not a mix of those 2 – Valentin Kuzub Sep 01 '11 at 15:17
  • Valentin Kuzub: I report progress by telling how much of the nodes I've processed from the nodes that I've detected. As for why I do it in ReportProgress, that's because that method is executed on the GUI thread always. However I've changed my code to specifically ask for the Dispatcher of the GUI and run my delegates on there. – Roy T. Sep 02 '11 at 12:04

1 Answers1

0

Generally I don't use BackgroundWorker often but I can suggest the following:

Logic for DoWork - its executed on non UI thread

  1. get count of nodes so you can report real progress
  2. begin building tree ( and call Invoke on UI Dispatcher so UI thread is adding nodes) and report progress to ReportProgress as (already added nodes)/(total count nodes) while enumerating through all nodes

in ProgressChanged simply update some ProgressBar with new value

Valentin Kuzub
  • 11,703
  • 7
  • 56
  • 93