9

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?

Oscar Mederos
  • 29,016
  • 22
  • 84
  • 124
Thomas Stock
  • 10,927
  • 14
  • 62
  • 79

5 Answers5

11

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
  • 167,459
  • 57
  • 363
  • 519
  • 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 Nov 18 '09 at 10:06
  • "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 Nov 18 '09 at 10:07
  • 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 Nov 18 '09 at 10:13
  • 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 Nov 18 '09 at 10:23
  • 1
    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. – tranmq Nov 18 '09 at 10:48
  • is it sufficient to set the CanExecute on "IsBusySaving == false" of my SaveCommand? – Thomas Stock Nov 18 '09 at 10:51
  • 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 Nov 18 '09 at 11:57
3

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. :)

tranmq
  • 15,168
  • 3
  • 31
  • 27
0

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
  • 9,086
  • 2
  • 29
  • 28
  • 1
    (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 Nov 18 '09 at 10:08
  • yes indeed, I appriciate the answer but I don't think it fixes my problem. Heinzi's suggestion fixed it. – Thomas Stock Nov 18 '09 at 10:11
  • @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 Nov 18 '09 at 12:06
0

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.

AZ_
  • 21,688
  • 25
  • 143
  • 191
0

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.

  • 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 Jul 15 '10 at 11:17
  • Keith: In .Net 3.5 and above, consider lambda expressions to trim the state object if you don't need it. ThreadPool.QueueUserWorkItem(state => Save()); Perhaps a little more overhead here but the code is often made simpler with less delegate appeasing methods floating around. You can also use a lambda to cast the state object to whatever you need and pull out specific properties. ThreadPool.QueueUserWorkItem(state => Save(state as SaveArgs).Length, state as SaveArgs).TimeStamp); – Gusdor Jul 27 '11 at 09:12