3

I've an existing WPF application, which has several sections. Every section is a UserControl, that implements an interface.

The interface specify two methods: void LoadData([...]) and bool UnloadData().

Those method are called by the UI thread, so we need to do our work in backgroundworker if it's time consuming.

No problems with LoadData since we can update the UI asynchronously. The problem is with UnloadData().

This should return if we can really leave the current view.

This is computed with the current status of data(Saved/modified/Invalid):

  • Saved return true,
  • Invalid asks if you want to stay to save some correct data or leave without saving
  • Modified tell you that you can either cancel your change(return true), either continue to edit(return false), either save you current data(return true)

The problem is with the "Modified -> Save". This is a time consuming method, so to respect the philosophy of the application, we should run this in a background thread(with a busy indicator).

But if we just launch the thread and go to the next section, it will return "true" to the method call, and we will directly launch the next view.

In my case, loading the next view before our local data is saved can be a problem.

So:

Is there a way to wait on the background thread to finish before returning "true", WITHOUT blocking the UI?

public bool UnloadData(){
   if(...){
      LaunchMyTimeConsumingMethodWithBackgroundWorker();
      return true;//Only when my time consuming method ends
   }
   //[...]
}

Important EDIT Maybe I wasn't clear enought: I know how to use a BackgroundWorker, or TPL. My problem is that the parent class(the one which call the UnloadData()" is a class that I cannot edit(for multiple reasons: It's in another DLL that will not be reloaded, it already works with 70+ userControls, all in separate projects(dll), loaded by reflection.

This wasn't my choice, I don't find it good, but I've to deal with it now. I'm mostly looking for way to make my method wait on the return of my method. I'm not sure if it is possible. But I'm looking for a workaround, it will spare me weeks of works.

J4N
  • 19,480
  • 39
  • 187
  • 340

6 Answers6

6

Ok now I'm excited, because I think I may have discovered something on my own...

So, what you do is this: You create a DispatcherFrame, push that frame onto the Dispatcher, and in the RunWorkerCompleted you set the Continue of the Frame to false.

This is the code so far:

public void Function()
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += TimeConsumingFunction;
    var frame = new DispatcherFrame();
    worker.RunWorkerCompleted += (sender, args) =>
                                     {
                                         frame.Continue = false;
                                     };
    worker.RunWorkerAsync();
    Dispatcher.PushFrame(frame);
}

private void TimeConsumingFunction(object sender, DoWorkEventArgs doWorkEventArgs)
{
    Console.WriteLine("Entering");
    for (int i = 0; i < 3; i++)
    {
        Thread.Sleep(1000);
    }
    Console.WriteLine("Exiting");
}

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    Function();
    Console.WriteLine("Returns");
}
AkselK
  • 2,563
  • 21
  • 39
  • I cannot change the way that my userControls method is called(70+ user controls already implements that expects to be called with the UI thread). So this has to be done in my method. And if I use the background thread, the method will returns before my time consuming method is finished. – J4N Dec 18 '12 at 14:27
  • So if I'm understanding this correctly, you call a method on the UI thread, which should not return until the save function has completed, but you do not want to block the UI thread? – AkselK Dec 18 '12 at 14:29
  • Exactly(but it's not me which make the call from the UI thread, it's the view manager, on which I can't do anything). – J4N Dec 18 '12 at 14:35
  • Try my edit. However, if you do not wish to block the UI thread, you are out of luck. A method that does not return on the UI thread WILL block the UI thread until it returns. – AkselK Dec 18 '12 at 14:38
  • It's what I was affraid, but I was hopping there is something which could check if we return, let the UI thread work a little, ... – J4N Dec 18 '12 at 14:43
  • I did some experimentation, and it seems I have solved it :) Try it out and let me know if it works! – AkselK Dec 18 '12 at 15:13
  • One word: GOD LIKE! I'm not sure to understand what is this "Frame" that we push, I will read more about this, but you response just saved my day! – J4N Dec 18 '12 at 15:53
  • To be honest, I don't know myself... If I were you I'd do some research in what side effects this might give, before you send it to production :P Good luck. – AkselK Dec 18 '12 at 16:53
  • Don't plays on words :P. I made some research, and it seems it makes some kind of "busy waiting". I think it sleeps sometime, letting the GUI thread updating the interface. It's not ideal, but with my requirement, it's the best thing we can find – J4N Dec 19 '12 at 13:44
2

You should implement a dependency property "IsBusy" of type bool, that you set to TRUE before starting the BackgoundWorker, and then to FALSE when the work is complete.

On the UI, you bind to that property whatever functionality you want disabled during the processing(like the button for loading the next view, etc.); or maybe showing a "Cancel" button.

You should not "wait" for the operation to complete, you can retrieve the result in an additional variable, that the BackgroundWorker will set:

    BackgroundWorker _bw;
    bool _returnValue = false;

    private void button_Click(object sender, RoutedEventArgs e)
    {  // if starting the processing by clicking a button
        _bw = new BackgroundWorker();

        IsBusy = true;

        _bw.DoWork += new DoWorkEventHandler(_bw_DoWork);

        _bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_bw_RunWorkerCompleted);

        _bw.RunWorkerAsync();
    }

    void _bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        IsBusy = false;

        // retrieve the result of the operation in the _returnValue variable
    }


    void _bw_DoWork(object sender, DoWorkEventArgs e)
    {
        _returnValue = UnloadData();
    }

    private bool UnloadData()
    {
        if (...)
        {
            LaunchTimeConsumingMethod();
            return true;
        }
        else
            return false;

        //etc ...
    }

    public bool IsBusy
    {
        get { return (bool)GetValue(IsBusyProperty); }
        set { SetValue(IsBusyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsBusy.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsBusyProperty =
        DependencyProperty.Register( ... )
Andy
  • 3,631
  • 2
  • 23
  • 32
2

You may be able to try using the new "await" features of .NET 4.5.

The await keyword allows you to await the completion of a Task object, without blocking the UI.

Try this modification:

public async bool UnloadData()
{
    if(...)
    {
        await Task.Factory.StartNew(() => 
            {
                LaunchMyTimeConsumingMethod();
            });
        return true;//Only when my time consuming method ends
    }
    //[...]
}
BTownTKD
  • 7,911
  • 2
  • 31
  • 47
  • 2
    "async bool UnloadData" is invalid syntax! The return type of an async method must be void, Task or Task – Kux Nov 26 '16 at 07:48
1

Treat UnloadData as a async operation and let the async/await features handle both the case when it completes synchronously and when it needs to complete asynchronously:

public async Task<bool> UnloadData(){
    if(...){
        // The await keyword will segment your method execution and post the continuation in the UI thread
        // The Task.Factory.StartNew will run the time consuming method in the ThreadPool
        await Task.Factory.StartNew(()=>LaunchMyTimeConsumingMethodWithBackgroundWorker());
        // The return statement is the continuation and will run in the UI thread after the consuming method is executed
        return true;
    }
    // If it came down this path, the execution is synchronous and is completely run in the UI thread       
    return false;
}


private async void button_Click(object sender, RoutedEventArgs e)
{
        // Put here your logic to prevent user interaction during the operation's execution.
        // Ex: this.mainPanel.IsEnabled = false;
        // Or: this.modalPanel.Visibility = Visible;
        // etc

        try
        {
            bool result = await this.UnloadData();
            // Do whatever with the result
        }
        finally
        {
            // Reenable the user interaction
            // Ex: this.mainPanel.IsEnabled = true;
        }
}

EDIT

If you can't modify the UnloadData, then just execute it on the ThreadPool, as @BTownTKD noted:

    private async void button_Click(object sender, RoutedEventArgs e)
{
        // Put here your logic to prevent user interaction during the operation's execution.
        // Ex: this.mainPanel.IsEnabled = false;
        // Or: this.modalPanel.Visibility = Visible;
        // etc

        try
        {

            // The await keyword will segment your method execution and post the continuation in the UI thread
            // The Task.Factory.StartNew will run the time consuming method in the ThreadPool, whether it takes the long or the short path
            bool result = await The Task.Factory.StartNew(()=>this.UnloadData());
            // Do whatever with the result
        }
        finally
        {
            // Reenable the user interaction
            // Ex: this.mainPanel.IsEnabled = true;
        }
}
Arthur Nunes
  • 6,718
  • 7
  • 33
  • 46
0

You probably should use TPL if your framework version is 4.0:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // this will work only if you're running this code from UI thread, for example, by clicking a button
Task.Factory.StartNew(() => UnloadData()).ContinueWith(t => /*update ui using t.Result here*/, uiScheduler);

Hope this helps.

Eugene Cheverda
  • 8,760
  • 2
  • 33
  • 18
  • But I don't need to update the UI in this method, I only need to return of my current method when the time consuming method exits – J4N Dec 18 '12 at 14:21
  • Process the results of UnloadData() in continuation method, here you can ask if it was successful - perform navigation to other view, if not - stay at the current. The continuation delegate won't start until the previous action completes. – Eugene Cheverda Dec 18 '12 at 14:25
  • The navigation to the other view is managed in another class, which I cannot edit – J4N Dec 18 '12 at 14:27
  • How does this class determine whether it is required to navigate to other view, where and how does this class work? – Eugene Cheverda Dec 18 '12 at 14:35
  • It's determined by the boolean that returns the implemented method "UnloadData". It's managed by the "ViewManager", which receive event from user controls. – J4N Dec 18 '12 at 14:50
0

You have to implement a callback function (RunWorkerCompleted), this is called when the background worker finishes.

Check out an example here: http://msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx

Chris McCabe
  • 1,010
  • 11
  • 20
  • I know that, but my method will have returned "true" long time ago and already changed the view. – J4N Dec 18 '12 at 14:20
  • One option is not to return true in that method but to call a method to update the view when the RunWorkerCompleted is called. EDIT: Sorry I didn't see that the interface you're using requires a boolean result. I'm not sure if any of this helps – Chris McCabe Dec 18 '12 at 14:22
  • Exactly, you see my problem right now ;). I need to implement this method, I cannot just do the navigation when I want :( – J4N Dec 18 '12 at 14:28
  • @J4N There may be something you can do by polling the IsBusy property of your BackgroundWorker, I'm not sure of the specifics without seeing the entire code but it might lead you to a solution. – Chris McCabe Dec 18 '12 at 15:01