In my MVVM Light application i want to show a notification before i do some some synchronous work that takes two or three seconds. I don't want the user to do anything while the work is being done so there is no need for async, Task and IProgress or backgroundworkers etc.
In a ViewModel i have this code. (Note that this is not located in the code-behind of the XAML-file but in a databound ViewModel)
void MyCommand(Project project)
{
NavigationService.AddNotification("Doin' it");
GetTheJobDone(project);
...
}
NavigationService adds the notification text to a databound ListView at the top of the client window.
My problem is that "Doin' it" show up after the project is loaded no matter how long time it took.
One example could be when I want to load and show a project. If the load part takes a few seconds the interface freezes without any information for the user what is happening.
EDIT
I add some more code due to requests. This code works fine.
The View that shows the notifications has this XAML
<ListView ItemsSource="{Binding Notifications}">
<ListView.ItemTemplate>
<DataTemplate>
<Label Foreground="LightGray" Content="{Binding Message}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The ViewModel that holds the notifications has this code
public int AddNotification(string message)
{
...
Notification note = new Notification { Message = message };
Notifications.Add(note);
...
}
The reason why you not getting a notification is that you running on the dispatcher thread (aka: UI thread), i.e. the main thread of your application, that is also used for redrawing your screen. So while you wait for the GetTheJobDone(project)
method to complete no screen drawing will be done. Therefore, you will need two threads to get this working, one to do the work and one to do the notification.
E.g.:
Lets move the work on its own thread, however, we then need to disable the screen (as by your design). This can be done by setting IsHitTestVisible
on the window (see end of post for a better approach).
textBox1.Text = "Started";
this.IsHitTestVisible = false; // this being the window
try
{
Task.Factory.StartNew(() => {
// handle errors so that IsHitTestVisible will be enabled after error is handled
try
{
this.GetTheJobDone();
Dispatcher.Invoke(() => {
// invoke required as we are on a different thead
textBox1.Text = "Done";
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() => {
this.textBox1.Text = "Error: " + ex.Message;
});
}
finally
{
Dispatcher.Invoke(() => {
this.IsHitTestVisible = true;
});
}
});
}
catch (Exception ex)
{
this.textBox1.Text = "Error: " + ex.Message;
this.IsHitTestVisible = true;
}
The extensive error handling is necessary as we blocked the interaction with the UI - not a good thing to do (see below) - and must make sure that it becomes available again should an error occur.
When using .NET 4.5 you can boil that code down to this:
try
{
textBox1.Text = "Started";
this.IsHitTestVisible = false; // this being the window
await Task.Factory.StartNew(() => {
this.GetTheJobDone();
});
textBox1.Text = "Done";
}
catch (Exception ex)
{
this.textBox1.Text = "Error: " + ex.Message;
}
finally
{
this.IsHitTestVisible = true;
}
Here no Dispatcher.Invoke
is required, as await marshals back to the calling thread. Also the error handling becomes much clearer as several execution paths are covered by a common error handling now.
However, instead of just blocking the UI it might be better to show a busy indicator (an overlay with a progress indicator), so that she knows what's going on. In this case setting IsHitTestVisible
is not necessary.
Here's my own solution based on AxelEckenbergers answer. As suggested I had to make it asynchronous to work. For now i don't lock the GUI, and it works fine.
In the base class for all my ViewModels i add a method called AddTask.
public Task AddTask(Action work,
string notification,
string workDoneNotification,
Action<Task> continueWith)
{
int notificationKey = NavigationService.AddNotification(notification,
autoRemove:false);
Task task = Task.Factory.StartNew(() =>
{
work.Invoke();
});
task.ContinueWith(t =>
{
NavigationService.RemoveNotification(notificationKey);
NavigationService.AddNotification(workDoneNotification);
if (continueWith != null)
{
continueWith.Invoke(t);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
return task;
}
The call looks like this
AddTask(() => GetTheJobDone(),
"Doin' it",
"It's done",
t => LoadProjects());