tags:

views:

1685

answers:

6

Hi,

I'm using the MVVM pattern in my first WPF app and have a problem with something quite basic I assume.

When the user hits the "save" button on my view, a command gets executed that calls the private void Save() in my ViewModel.

The problem is that the code in "Save()" takes some time to execute, so I'd like to hide the "Save" button in the UI view before executing the large chunk of code.

The problem is that the view doesn't update untill all code is executed in the viewmodel. How can I force the view to redraw and process the PropertyChanged events before executing the Save() code?

Additionally, I would like a reuseable way, so that I can easily do the same thing in other pages as well.. Anyone else made something like this already? A "Loading..." message?

+6  A: 

If it takes a long time, consider using a separate thread, for example by using a BackgroundWorker, so that the UI thread can stay responsive (i.e. update the UI) while the operation is performed.

In your Save method, you would

  • change the UI (i.e. modify some INotifyPropertyChanged or DependencyProperty IsBusySaving boolean which is bound to your UI, hides the Save button and maybe shows some progress bar with IsIndeterminate = True) and
  • start a BackgroundWorker.

In the DoWork event handler of your BackgroundWorker, you do the lengthy saving operation.

In the RunWorkerCompleted event handler, which is executed in the UI thread, you set IsBusySaving to false and maybe change other stuff in the UI to show that you are finished.

Code example (untested):

BackgroundWorker bwSave;
DependencyProperty IsBusySavingProperty = ...;

private MyViewModel() {
    bwSave = new BackgroundWorker();

    bwSave.DoWork += (sender, args) => {
        // do your lengthy save stuff here -- this happens in a separate thread
    }

    bwSave.RunWorkerCompleted += (sender, args) => {
        IsBusySaving = false;
        if (args.Error != null)  // if an exception occurred during DoWork,
            MessageBox.Show(args.Error.ToString());  // do your error handling here
    }
}

private void Save() {
    if (IsBusySaving) {
        throw new Exception("Save in progress -- this should be prevented by the UI");
    }
    IsBusySaving = true;
    bwSave.RunWorkerAsync();
}
Heinzi
thanks, I'll give it a try.
Thomas Stock
sorry I'm a total dumbo when it comes to threading. Inside the Save code I (sometimes) try to navigate to another page. But because I'm in another thread, this gives a runtime error. I guess I have to do a callback to the original thread and navigate from there to the other page. But I'll try this myself, I'm sure it's not hard to communicated with the original thread.
Thomas Stock
"The calling thread cannot access this object because a different thread owns it." is the message I get. If you know by heart what I need, let me know :-)
Thomas Stock
RunWorkerCompleted, that's what I needed. Thanks a lot. Also awesome to see I can declare the eventhandler like that, didnt know that!
Thomas Stock
Yes, RunWorkerCompleted is exactly the right place for that since it runs in the UI thread. If you need to change UI stuff *during* the Save operation, you can either use `Application.Current.Dispatcher.Invoke` or the `ReportProgress` method/`ProgressChanged` event combination of the `BackgroundWorker`.
Heinzi
You are my hero.
Thomas Stock
You still need to disable your save button when IsBusySaving == true, or you have to provide a lock in the DoWork in order to NOT getting yourself into a race condition.
mqbt
is it sufficient to set the CanExecute on "IsBusySaving == false" of my SaveCommand?
Thomas Stock
I guess so. If you want to make something fancy, you can also add some "Saving in progress..." text on your UI whose visibility is bound to IsBusySaving via a `BooleanToVisibilityConverter`.
Heinzi
yeah I already use that to hide the button. thanks.
Thomas Stock
A: 

You could always do something like this:

public class SaveDemo : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  private bool _canSave;

  public bool CanSave
  {
    get { return _canSave; }
    set
    {
      if (_canSave != value)
      {
        _canSave = value;
        OnChange("CanSave");
      }
    }
  }

  public void Save()
  {
    _canSave = false;

    // Do the lengthy operation
    _canSave = true;
  }

  private void OnChange(string p)
  {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
    {
      handler(this, new PropertyChangedEventArgs(p));
    }
  }
}

Then you could bind the IsEnabled property of the button to the CanSave property, and it will automatically be enabled/disabled. An alternative method, and one I would go with would be to use the Command CanExecute to sort this, but the idea is similar enough for you to work with.

Pete OHanlon
(1) If you set _canSave instead of CanSave, OnChange will not be raised. (2) I don't think it will work, since Save runs in the UI thread, so the WPF UI won't be updated until Save has finished.
Heinzi
yes indeed, I appriciate the answer but I don't think it fixes my problem. Heinzi's suggestion fixed it.
Thomas Stock
@Heinzi - good spot on the CanSave, and yes it will work because the notification change is raised at the start of the save operation - hence, the UI updates at that point.
Pete OHanlon
+1  A: 

You're using MVVM pattern, so your Save Button's Command is set to an instance of the RoutedCommand object which is added to the Window's CommandBindings collection either declaratively or imperatively.

Assuming that you do it declaratively. Something like

<Window.CommandBindings>
 <CommandBinding
  Command="{x:Static namespace:ClassName.StaticRoutedCommandObj}"
  CanExecute="Save_CanExecute"
  Executed="Save"
 />
</Window.CommandBindings>

For the handler of Executed routed event, your Save() method, on entry, you set a variable to false, on return you set it back to true. Something like.

void Save(object sender, ExecutedRoutedEventArgs e)
{
 _canExecute = false;
 // do work
 _canExecute = true; 
}

For the handler of the CanExecute routed event, the Save_CanExecute() method, you use the variable as one of the condition.

void ShowSelectedXray_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
 e.CanExecute = _canExecute && _others;
}

I hope I am clear. :)

mqbt
A: 

You can accomplish this by the following code..

Thread workerThread = null;
void Save(object sender, ExecutedRoutedEventArgs e)
{
workerThread = new Thread(new ThreadStart(doWork));
SaveButton.isEnable = false;
workerThread.start();
}

do all your lengthy process in dowork() method

in some other method...

workerThread.join();
SaveButtton.isEnable = true;

This will cause to run save lengthy process in another thread and will not block your UI, if you want to show an animation while user click on save button then show some progress bar like iPhone etc... give me feedback i'll try to help you even more.

Aizaz
A: 

Late answer, but I figured it'd be good to input a bit as well.

Instead of creating your own new thread, it would probably be better to leave it up to the threadpool to run the save. It doesn't force it to run instantly like creating your own thread, but it does allow you to save threading resources.

The way to do that is:

ThreadPool.QueueUserWorkItem(Save);

The problem with using this approach, as well, is that you're required to have your "Save()" method take in an object that will act as a state. I was having a similar problem to yours and decided to go this route because the place that I'm working is very Resource-Needy.

Keith B
thanks for your answer. I used the accepted answer in my application and it works fine. I don't know how the resource usage is as compared to your solution but the backgroundworker is very convenient to work with.
Thomas Stock