In this part, we will add a busy indicator.
As you know, communication with server takes some time. Even in our small project loading might take several seconds so it would be a good idea to notify the user that something is happening in background.
So, open your solution from part 9 or grab source codes from Coproject site and let’s start.
First of all, add a BusyIndicator control to ToDoListsView. So, add this namespace using:
xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
and paste this control to the end of LayoutRoot (below ActiveItem and Toolbar):
<toolkit:BusyIndicator IsBusy="{Binding IsBusy}" Grid.RowSpan="2" />
Then we must implement the IsBusy property on ToDoListsViewModel.
The easiest way to implement busy indication would be to add IsBusy property to ToDoListsViewModel:
private bool _isBusy; public bool IsBusy { get { return this._isBusy; } set { this._isBusy = value; NotifyOfPropertyChange(() => IsBusy); } }
Then, we just set it to true at the beginning of LoadData function and to false at the end. Try it, it should work well.
Note: if the loading is too fast so you cannot see the BusyIndicator, open CoprojectService and add Thread.Sleep(1000); above ‘return’ statement to the proper GetXXX function (GetToDoListsWithItems to be exact).
Although this approach works, you can see problems if more than one loading is done at a time:
- the first action starts and sets IsBusy to true
- the other action starts and sets IsBusy to true
- the first action completes and sets IsBusy to false
- the other action is still running but IsBusy is already set to false
So, it would be a good idea to have something like BusyWatcher – a class that has a hidden counter inside, actions just increment or decrement it as they start or finish, and IsBusy property returns whether the counter is greater than 0 or not. One of possible implementations is as follows. Put it into Framework/BusyWatcher.
public class BusyWatcher : PropertyChangedBase { int _counter; public bool IsBusy { get { return _counter > 0; } } public void AddWatch() { if (Interlocked.Increment(ref _counter) == 1) { NotifyOfPropertyChange(() => IsBusy); } } public void RemoveWatch() { if (Interlocked.Decrement(ref _counter) == 0) { NotifyOfPropertyChange(() => IsBusy); } } }
Next, extract interface IBusyWatcher from it (IsBusy, AddWatch, RemoveWatch) and make BusyWatcher export this interface:
public interface IBusyWatcher { bool IsBusy { get; } void AddWatch(); void RemoveWatch(); }
[Export(typeof(IBusyWatcher))] public class BusyWatcher : PropertyChangedBase, IBusyWatcher ...
Now, let’s use this watcher in our view model. Replace the IsBusy property in ToDoListsViewModel with the following:
[Import(RequiredCreationPolicy = CreationPolicy.NonShared)] public IBusyWatcher Busy { get; set; }
And setting of IsBusy in LoadData change to their respective equivalents of Busy.AddWatch/RemoveWatch.
Finally change binding of BusyIndicator int ToDoListsView to:
<toolkit:BusyIndicator IsBusy="{Binding Busy.IsBusy}" Grid.RowSpan="2" />
OK, this looks much better but what if an exception occurs between AddWatch and RemoWatch calls? BusyWatcher would stay in as busy forever. Of course, we could wrap actions with a try block and put RemoveWatch into a finally statement.
Another way is to make use of ‘using’ statement. The trick is that we create a disposable ticket for the using statement. And when the ticket gets disposed of, it will RemoweWatch.
Add this class into BusyWatcher:
public class BusyWatcherTicket : IDisposable { IBusyWatcher _parent; public BusyWatcherTicket(IBusyWatcher parent) { _parent = parent; _parent.AddWatch(); } public void Dispose() { _parent.RemoveWatch(); } }
Also add this function into BusyWatcher and put its definition into IBusyWatcher too:
public BusyWatcherTicket GetTicket() { return new BusyWatcherTicket(this); }
Now, we can use it as follows:
public IEnumerable<IResult> LoadData(string filter) { using (Busy.GetTicket()) { CoprojectContext context = new CoprojectContext(); ...
Lists = result.Result.Entities; NotifyOfPropertyChange(() => Lists); } }
Note that in this scenario, you have separate BusyWatcher for every screen (i.e., separate BusyIndicator for every view). But if you, for example, wanted to have one BusyIndicator for the whole application (e.g., in a status bar), we could use a shared BusyWatcher for all view models and add a BusyIndicator into ShellView and bind it to this BusyWatcher. No big deal.