Contents
As many of you may know, I write quite a lot about UI work and WPF technology, and I have in the past written articles about Threading, and have also written a great lot about my own WPF MVVM Framework Cinch, which includes a lot of stuff to get you up and running doing WPF the MVVM way.
Well, one thing that has always bugged me while working with WPF is threading and doing something in the background and keeping my UI nice and free to do other stuff. Sure I can spawn a new Thread
or use the ThreadPool
and even use the funky BackgroundTaskManager
class inside of Cinch which does help a lot in terms of fetching data on an internally held BackgroundWorker
and is completely unit testable, as discussed in this Cinch article. I just felt that more improvements could be made.
For example, what I would want to be able to do, is have a ViewModel (to allow Binding and testing) that was being used as a DataContext
for some View, to be able to accept some parametised work delegate
that would be performed on a background thread, and would show some busy animation or status while the threading operation was happening, and would then either show an error status in the View somewhere if the background threading operation failed, or would show me the actual data that the background thread fetched that I requested in the parametised work delegate
, if there was no failure while fetching the relevant data.
I should point out that the code contained here does spawn a new BackgroundWorker
per threading operation, where each operation is expected to return some single definable bit of data, such as a single List<SomeClass>
, or even some other very expensive time consuming data to fetch. My intent was never to have some global threading manager that fetches all and sundry in one hit, it's more micro managing the data.
In laymen's terms, think of it like this (where "I" and "Me" in this italic paragraph means the demo app code provided in this article ):
You want me to show a List<Contact>
that could take some time to fetch, fair enough. I'll create a BackgroundWorker
to do that and manage that background activity, and let you know how it's going, and when I am done, I'll return you a List<Contact>
, or some error message. Oh, and you also want me to show a List<BankAccount>
on the same View that will also take some time to fetch. Well now, for that, I will need to return another type of List
. In fact, it will be a List<BankAccount>
so I am going to be needing another BackgroundWorker
to do that, and return you your List<BankAccount>
.
I know this could potentially spawn a few threads, but internally, the BackgroundWorker
makes use of the ThreadPool
, which is ace, so I do not feel it's an issue, as the management of Thread
s is done for us by the .NET Framework. Obviously, if you have hundreds of lists of data on one View, this article may not be for you at all, and you should stop reading right here. If on the other hand you have a few lists of data or a few expensive bits of data to fetch, this code could well be for you.
Anyway, that is how I see it working, and after messing around for a while, I think I have done just that.
There are a few assumptions that I have made, which are as follows:
- That people are using WPF, and are pretty competent with it. This is not a beginner article at all.
- That people are using the MVVM pattern.
- That people think it's a good idea to have parts of your View show error messages if the data that was supposed to be fetched failed to happen.
- That people are happy with the idea of an item of data (such as a
List
of data) or expensive bits of data being fetched by a dedicated BackgroundWorker
manager object, which we will get to later.
The problem is really like this: from what I have seen, very few people take the time to show the user much feedback at the best of times, let alone when a long running threading operation is happening. In fact, some UIs just boldly do everything on the UI thread, and let the user wait. OK, some people do change the icon to an hour-glass or something, and disable buttons etc., while something is happening.
Wouldn't it be better if we had some component that kept the user in the loop? They start a long running operation either by requesting it or by opening up some View that requires it, and when that happens, the user is constantly shown what is going on as a progress bar is shown while we are doing the work, and if it fails, we show them why, right there in the UI, where it's visible, not in some MessageBox
that disappears as soon as they accept "OK", and then try and ring support only to be asked what the MessageBox
message said. Oh, I closed that, sorry. And of course, when it all goes to plan, simply hide the progress bar, do not show any failure UI, and just show them what they wanted that has now been fetched.
That is the problem, the way I see it.
So what is the solution? Well, the solution is obviously to come up with something that fixes the problem, right?
Well, this article does fix all of the things mentioned above in the problem description. The way it does it, is by using various bits of WPF technology, and threading bits and pieces.
As I previously mentioned, the code supplied in the attached demo app is assuming that you are using the MVVM pattern. I am not saying you could not get it to work without using MVVM; it's just, I think MVVM is fab, and it works, and that is the only way I will be describing in this article's text.
The demo code makes use of a simple idea; we use a WPF UserControl
to wrap a bit of content, which would be the UI data that you wish to fetch in a background thread.
So when it runs, it looks something like this:
The attached code makes use of the AdornerLayer
to show different Adorners depending on the state of the background threading operation. If the background threading operation is busy, the BusyAdorner
is shown. If the background threading operation failed and is not busy, the FailedAdorner
is shown. If the background threading operation is not busy and not failed, hide both the BusyAdorner
and FailedAdorner
, which just leaves the original data shown in the UI, which has now been fetched on a background thread.
You can read more about how the Adorners work in the Adorners section below.
In this series of sub sections, I will outline how the attached demo code is structured. It should be noted that you will not need to know about a lot of this, you would simply include it in your project and do what is recommended in the How to Go About Using This Idea in Your Own WPF App section. But if you are like me, you will want to know about the details in full before knowing what you need to do to use it, so that is what this section is all about.
The demo app attached obviously has to demonstrate the total idea. As such, there is a retarded bit of code that allows the user to pick or choose whether the background threading operation should fail during runtime. The user is able to do this by clicking a button on the demo code's UI.
Now this is obviously only test code, and should never be used in production code; all it does is toggle a "ShouldFail
" flag that is checked within the background thread delegate
. Which is like this in the demo code:
Func<Dictionary<String, Object>,
ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
try
{
#region TEST EXAMPLE CODE, YOU SHOULD NOT DO THIS IN PRODUCTION CODE
if (ShouldFail)
{
throw new InvalidOperationException(
"InvalidOperationException occurred\r\n\r\n" +
"This Exception has been raised inside " +
"the Window1ViewModel delegate " +
"which is the actual payload for the " +
"ThreadableItemViewModel<T> TaskFunc delegate.\r\n\r\n" +
"Which is obvioulsy not what you would " +
"do in a production system.\r\n\r\n" +
"You would more typically catch your own business Exceptions " +
"(say from talking to WCF) and then rethrow them. " +
"This just demomstrated how all this hangs together");
}
else
{
List<StuffData> data = new List<StuffData>();
for (int i = 0; i < (Int32)inputParams["loopMax"]; i++)
{
data.Add(new StuffData(String.Format("The Text Is {0}",i),i));
Thread.Sleep(5); }
return new ThreadableItem<List<StuffData>>(data, String.Empty);
}
#endregion
}
catch(Exception ex)
{
return new ThreadableItem<List<StuffData>>(null, ex.Message);
}
};
It is just there to demonstrate how the code is intended to work. In real production code, you would not do this; you would more likely do something like this:
Func<Dictionary<String, Object>, ThreadableItem<List<Client>>> taskFunc = (inputParams) =>
{
try
{
try
{
Service<IGateway>.Use((client) =>
{
RetrieveDataByQueryRequest request = new RetrieveDataByQueryRequest();
request.Query = new Query().SelectAll(BusinessEntityType.Client)
.Where(new Filter("BusinessAreaId",
FieldOpeartor.Equals,(Int32)inputParams["businessArea"]);
RetrieveDataByQueryResponse response =
(RetrieveDataByQueryResponse)client.Execute(request);
return new ThreadableItem<List<Client>>(response.Clients, String.Empty);
});
}
catch (FaultException<SerializationException> sex)
{
throw new BusinessException("A serialization issue has occurred");
}
catch (FaultException<InvalidArgumentException> iaex)
{
throw new BusinessException("One of the arguments is invalid");
}
}
catch(Exception ex)
{
return new ThreadableItem<List<Client>>(null, ex.Message);
}
};
While this may look a bit hairy right now, do not worry, we will be going through that rather nasty looking Func<T,TResult> delegate
declaration a bit later. The important thing to note is that the demo code uses some gash test code that you should see as just that demo code that needs replacing with your real code, something like the code chunk seen above.
As I stated, the demo code uses the MVVM pattern. You might ask yourself why. The reasons are simple; MVVM allows us to bind directly to our ViewModel, and the ViewModel also serves as a very nice unit testing entry point. As such, there are various ViewModels in the attached demo code, the main ones being:
ViewModelBase
: Simple INotifyPropertyChanged
base class for all other ViewModels to inherit from.
Here is what this looks like, nothing too fancy:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace ThreadingComponent
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, args);
}
}
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
ThreadableItemViewModelBase
: This is a base class for ThreadableItemViewModel<T>
based ViewModels, and allows the UserControl that handles the AdornerLayer and wraps the data to hook up INotifyPropertyChanged
property changed watchers without having to care about the actual generic type of the current background threading operation. This class basically just exposes some common properties that all ThreadableItemViewModel<T>
based ViewModels will need to use.
This is what this looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ThreadingComponent
{
public class ThreadableItemViewModelBase : ViewModelBase
{
#region Data
private Boolean isBusy = false;
private Boolean failed = false;
private String errorMessage = String.Empty;
#endregion
#region Public Properties
public bool IsBusy
{
get { return isBusy; }
set
{
isBusy = value;
NotifyPropertyChanged("IsBusy");
}
}
public bool Failed
{
get { return failed; }
set
{
failed = value;
NotifyPropertyChanged("Failed");
}
}
public String ErrorMessage
{
get { return errorMessage; }
set
{
errorMessage = value;
NotifyPropertyChanged("ErrorMessage");
}
}
#endregion
}
}
ThreadableItemViewModel<T>
: This generic ViewModel inherits from ThreadableItemViewModelBase
. Its job is to manage the background threading operation. As such, it has various properties that facilitate the management of the running of the background threading activity. The T
generic should be the Type
that you want for the background threading activity. So for example, if I expected to get a List<Client>
back, T
would be List<Client>
.
You would need to have one of these ThreadableItemViewModel<T>
for each background activity you wish to perform. In the demo app, that is only one List
of some imaginary Model data called "StuffData
", so in my main ViewModel (Window1ViewModel
), I have a single instance of ThreadableItemViewModel<List<StuffData>>
which is used to manage the retrieval of a List<StuffData>
.
Here is what this looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading;
namespace ThreadingComponent
{
public class ThreadableItemViewModel<T> : ThreadableItemViewModelBase
{
#region Data
private ThreadableItem<T> data;
private Func<Dictionary<String,Object>,ThreadableItem<T>> taskFunc;
private BackgroundTaskManager<ThreadableItem<T>> bgWorker;
private Dictionary<String, Object> parameters = null;
#endregion
#region Private Methods
private void SetupWorker()
{
if (taskFunc == null)
throw new NullReferenceException("TaskFunc can not be null");
bgWorker = new BackgroundTaskManager<ThreadableItem<T>>(
() =>
{
return taskFunc(Parameters);
},
(result) =>
{
Data = result;
});
BgWorker.BackgroundTaskStarted -= BackgroundTaskStarted;
BgWorker.BackgroundTaskCompleted -= BackgroundTaskCompleted;
BgWorker.BackgroundTaskStarted += BackgroundTaskStarted;
BgWorker.BackgroundTaskCompleted += BackgroundTaskCompleted;
}
private void BackgroundTaskCompleted(object sender, EventArgs args)
{
IsBusy = false;
Failed = !String.IsNullOrEmpty(Data.Error);
}
private void BackgroundTaskStarted(object sender, EventArgs args)
{
IsBusy = true;
Failed = false;
}
#endregion
#region Public Methods
public void Run()
{
SetupWorker();
bgWorker.RunBackgroundTask();
}
#endregion
#region Public Properties
public Func<Dictionary<String,Object>,ThreadableItem<T>> TaskFunc
{
set
{
taskFunc = value;
}
}
public Dictionary<String, Object> Parameters
{
get { return parameters; }
set { parameters = value; }
}
public BackgroundTaskManager<ThreadableItem<T>> BgWorker
{
get { return bgWorker; }
}
public ThreadableItem<T> Data
{
get { return data; }
set
{
data = value;
if (data != null)
this.ErrorMessage = data.Error;
NotifyPropertyChanged("Data");
}
}
#endregion
}
}
The most important things to note in this bad boy are the public properties:
TaskFunc
: Which is a Func delegate
that is the actual background work you want done.
Parameters
: A Dictionary<String,Object>
which is fed into the TaskFunc
, Func delegate
which is your parameters collection, which may be useful when running the background operation.
BgWorker
: The actual BackgroundTaskManager<T>
which is exposed to allow a Unit Test to maybe set up a AutoResetEvent
and only Wait
a specific amount of time for the operation to happen, before assuming it failed. The BackgroundTaskManager<T>
class is straight out of my Cinch MVVM framework, and is discussed in this Cinch article.
Data
: Is the actual data which is of Type ThreadableItem<T>
, so what does one of those look like then? Well, ThreadableItem<T>
is a simple class that represents the result of the background operation, so it has a DataObject
and an Error
. Only one of which is expected to be an actual value at any one time.
If the operation ran successfully, then the ThreadableItem<T>
's DataObject
will be an instance of T
that the user asked for, and the ThreadableItem<T>
's Error
will be an empty string.
If the operation failed, the ThreadableItem<T>
's DataObject
will be null
, and the ThreadableItem<T>
Error
will be an Exception message string.
For clarity, here is what the ThreadableItem<T>
class looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ThreadingComponent
{
public class ThreadableItem<T> : ViewModelBase
{
#region Data
private T dataObject;
private String error;
#endregion
#region Constructor
public ThreadableItem(T dataObject, String error)
{
this.DataObject = dataObject;
this.Error = error;
}
#endregion
#region Public Properties
public T DataObject
{
get { return dataObject; }
set
{
dataObject = value;
NotifyPropertyChanged("DataObject");
}
}
public String Error
{
get { return error; }
set
{
error = value;
NotifyPropertyChanged("Error");
}
}
#endregion
}
}
So far, all we have covered in the ViewModels, you will most likely not change, so there is one more to cover which is how to actually use the ones we have seen above. In the demo app, there is a single Window
called Window1.xaml, this has a single ViewModel (to allow binding) called Window1ViewModel
, which manages all the operations that Window1
wants to do.
Window1ViewModel
is an exemple of how to use the rest of the codebase, though as I previously stated, does have test code in it so you can see how all this fits together, and you will need to change it in your production code. Again, I showed you an example of that earlier.
Whilst not strictly part of the article's main thrust, I would recommend using some sort of DelegateCommand
, or Josh Smith RelayCommand
, or Marlon Grech SimpleCommand
(which is what I use), all of which allow your UI to bind to a ViewModel exposed ICommand
and actually run code in the ViewModel. The reason I would recommend ICommand
s is that you can disable a button in the ICommand.CanExecute
directly in the ViewModel if there is a background threading operation in progress. For an example of this, see Window1ViewModel
.
Obviously, this only applies if the threading operation happens as a result of a button click or something the user initiated.
Since this article is all about a background threading component/idea, you would expect there to be loads to say about Threads. Well, actually a lot of that is abstracted from you.
The steps involved are pretty much like this (you can use the attached demo Window1ViewModel
code as a basis for writing your own ViewModel code to use this article's ideas/concepts).
Step 1: Expose property and pick T
Create a ViewModel, and expose a ThreadableItemViewModel<T>
as a public property which you can then bind to using a ThreadableHostControl
UserControl
which is described in the next section, where the generic T
must obviously be qualified with the correct type; for example, here is a valid property declaration:
public ThreadableItemViewModel<List<StuffData>> ThreadVM
{
get { return threadVM; }
}
Step 2: Wire up background work delegate
Now that you have exposed ThreadableItemViewModel<T>
as a public property, and picked a return type for it, we can look at how to get the results in a background thread. This is easily achieved; all we have to do is set up the background work delegate, which is done as follows:
Func<Dictionary<String, Object>, ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
try
{
#region TEST EXAMPLE CODE, YOU SHOULD NOT DO THIS IN PRODUCTION CODE
if (ShouldFail)
{
throw new InvalidOperationException(
"InvalidOperationException occurred\r\n\r\n" +
"This Exception has been raised inside the Window1ViewModel delegate " +
"which is the actual payload for the " +
"ThreadableItemViewModel<T> TaskFunc delegate.\r\n\r\n" +
"Which is obvioulsy not what you would do in a production system.\r\n\r\n" +
"You would more typically catch your own business Exceptions " +
"(say from talking to WCF) and then rethrow them. " +
"This just demomstrated how all this hangs together");
}
else
{
List<StuffData> data = new List<StuffData>();
for (int i = 0; i < (Int32)inputParams["loopMax"]; i++)
{
data.Add(new StuffData(String.Format("The Text Is {0}",i),i));
Thread.Sleep(5); }
return new ThreadableItem<List<StuffData>>(data, String.Empty);
}
#endregion
}
catch(Exception ex)
{
return new ThreadableItem<List<StuffData>>(null, ex.Message);
}
};
threadVM.TaskFunc = taskFunc;
The important thing to observe here are that we return a ThreadableItem<T>
where T
is of type List<StuffData>
. And remember, a Func<T,TResult>
is nothing more than a delegate
that looks like this:
public delegate TResult Func<T, TResult>(T arg);
So really, all we are just saying is that we have a delegate that takes Dictionary<String, Object>
as an input parameter, and it returns ThreadableItem<List<StuffData>>
as a return value. Simple, right?
The Dictionary<String, Object>
acts as a collection of parameters that may be needed by the thread work item, so we need to configure it like so:
Dictionary<String, Object> parameters = new Dictionary<String, Object>();
parameters.Add("loopMax", 30000);
threadVM.Parameters = parameters;
If your work delegate does not require any parameters to do its job, simply set Parameters = null
.
Step 3: Run the Background Work Item
The last thing to do is to run the background work item, which is as easy as this:
threadVM.Run();
Which will do the following inside the ThreadableItem<T>
code:
public void Run()
{
SetupWorker();
bgWorker.RunBackgroundTask();
}
private void SetupWorker()
{
if (taskFunc == null)
throw new NullReferenceException("TaskFunc can not be null");
bgWorker = new BackgroundTaskManager<ThreadableItem<T>>(
() =>
{
return taskFunc(Parameters);
},
(result) =>
{
Data = result;
});
BgWorker.BackgroundTaskStarted -= BackgroundTaskStarted;
BgWorker.BackgroundTaskCompleted -= BackgroundTaskCompleted;
BgWorker.BackgroundTaskStarted += BackgroundTaskStarted;
BgWorker.BackgroundTaskCompleted += BackgroundTaskCompleted;
}
This code makes use of a funky BackgroundTaskManager
class inside of Cinch which does help a lot in terms of fetching data on an internally held BackgroundWorker
and is completely unit testable, as discussed in this Cinch article. The important thing to note here is to see how this simply calls the original taskFunc
property, which is the one that you just specified using the Func<T,TResult> delegate
, remember?
Func<Dictionary<String, Object>, ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
....
....
....
....
};
threadVM.TaskFunc = taskFunc;
It also passes the Func<T,TResult> delegate
the parameters Dictionary<String, Object>
, which are the parameters that can be used inside the threading delegate work item.
I guess I better prove this all works with a screenshot. Here is a screenshot using the worker item code above:
Word of warning
The code above is just there to demonstrate how the failure code is intended to work. A reader alerted me to the fact that the previous article (yes, this is being posted again) took up 400MB of RAM. Now, the reason that was is my old worker delegate used to create 5,000,000 objects in memory, which as you can imagine was a real issue. Now, we would never actually do that in a production environment. I have modified the code to use a much more modest 30000 objects, which uses like 19MB of RAM. So thanks to that user for spotting that. I am glad it was not something stupid I had done. Phew.
In fact, after I published it again, the same user Insomniac Geek came up with another rather sensible suggestion, which is to use a few items (1000) and use a System.Threading.Thread.Sleep(5)
to simulate some work, so that is what I do now. As even with 30,000 items, the background thread happened in milliseconds. So thanks user Insomniac Geek.
In real production code, you would not do this, you would more likely do something like this:
Func<Dictionary<String, Object>, ThreadableItem<List<Client>>> taskFunc = (inputParams) =>
{
try
{
try
{
Service<IGateway>.Use((client) =>
{
RetrieveDataByQueryRequest request = new RetrieveDataByQueryRequest();
request.Query = new Query().SelectAll(BusinessEntityType.Client)
.Where(new Filter("BusinessAreaId",
FieldOpeartor.Equals,(Int32)inputParams["businessArea"]);
RetrieveDataByQueryResponse response =
(RetrieveDataByQueryResponse)client.Execute(request);
return new ThreadableItem<List<Client>>(response.Clients, String.Empty);
});
}
catch (FaultException<SerializationException> sex)
{
throw new BusinessException("A serialization issue has occurred");
}
catch (FaultException<InvalidArgumentException> iaex)
{
throw new BusinessException("One of the arguments is invalid");
}
}
catch(Exception ex)
{
return new ThreadableItem<List<Client>>(null, ex.Message);
}
};
threadVM.TaskFunc = taskFunc;
Now that you know there is a ThreadableItemViewModel<T>
property exposed that makes use of an internal ThreadableItem<T>
, we can imagine making use of that in the UI. This is done via exposing a ThreadableItemViewModel<T>
property in your own ViewModel, like this:
public ThreadableItemViewModel<List<StuffData>> ThreadVM
{
get { return threadVM; }
}
In this case, the generic T
is obviously a List<StuffData>
. So how do we then use this in the actual View? Well, let's have a look, shall we?
<local:ThreadableHostControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ThreadableItem="{Binding ThreadVM}">
<ListView Background="WhiteSmoke" BorderBrush="Black" BorderThickness="5"
ItemsSource="{Binding ThreadVM.Data.DataObject}">
<ListView.View>
<GridView>
<GridViewColumn Header="Text" Width="500"
DisplayMemberBinding="{Binding Path=Text}"/>
<GridViewColumn Header="Age" Width="100"
DisplayMemberBinding="{Binding Path=Age}"/>
</GridView>
</ListView.View>
</ListView>
</local:ThreadableHostControl>
It can be seen that there is a ThreadableHostControl
UserControl
which makes use of this ThreadableItemViewModel<List<StuffData>> ThreadVM
property exposed by the ViewModel.
There are two clever things going on here:
Let Content Be Content
In WPF, there are loads of different ways to do things, but I always like to keep my XAML as clean as I can. Part of the trick is understanding the framework, but also knowing what classes support what properties. A UserControl
has a Content
property, so let's just use that to host the data that we want to show which is fetched in the background thread. In this case, this will be a List<StuffData>
bound to a ListView
.
We could have done some tricks with several UI controls having their Visibility
properties toggled, but that means more XAML, ouch. A better way is to let the content be content, and then we can host extra content on top of the content in a layer called the AdornerLayer
.
If you do not know about the AdornerLayer
, you should read this MSDN link: http://msdn.microsoft.com/en-us/library/ms743737.aspx.
Supporting Adorners
The ThreadableHostControl.ThreadableItem
property is bound to an instance of ThreadableItemViewModel<T>
which inherits from ThreadableItemViewModelBase
, which as I mentioned earlier was the base class for all ThreadableItemViewModel<T>
that supported a couple of properties namely:
IsBusy
Failed
ErrorMessage
So how does the ThreadableHostControl
UserControl
make use of the ThreadableItemViewModelBase
property that it gets via a Binding
? Well, that is quite interesting; let's have a look, shall we?
The ThreadableHostControl
s ThreadableItem
DependencyProperty
changed event calls an internal method called SetupPropertyWatcher()
:
private static void OnThreadableItemChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
((ThreadableHostControl)d).SetupPropertyWatcher(
(ThreadableItemViewModelBase)e.NewValue);
}
}
So looking into the SetupPropertyWatcher()
method, we can see that it sets up the INotifyPropertyChanged
property watchers for various properties of the ThreadableItemViewModelBase
DP instance:
private void SetupPropertyWatcher(ThreadableItemViewModelBase item)
{
if (threadableItemObserver != null)
{
threadableItemObserver.UnregisterHandler(n => n.IsBusy);
threadableItemObserver.UnregisterHandler(n => n.Failed);
}
threadableItemObserver = new PropertyObserver<ThreadableItemViewModelBase>(item);
threadableItemObserver.RegisterHandler(n => n.IsBusy, this.IsBusyChanged);
threadableItemObserver.RegisterHandler(n => n.Failed, this.FailedChanged);
}
Note: I am making use of Josh Smith's most excellent PropertyObserver to rig up watcher methods on the the original INPC object.
If we follow one of these through, say the Failed
-> FailedChanged()
method, we will see what happens:
private void FailedChanged(ThreadableItemViewModelBase vm)
{
if (vm.IsBusy)
return;
adornerLayer = AdornerLayer.GetAdornerLayer(this);
if (shouldThrowExceptionOnNullAdornerLayer && adornerLayer == null)
throw new NotSupportedException(
"The ThreadableHostControl will only work correctly\r\n" +
"if there is an AdornerLayer found and it is not null");
if (adornerLayer != null)
{
if (vm.Failed)
{
SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
failedAdorner = new FailedAdorner(this, vm.ErrorMessage);
adornerLayer.Add(failedAdorner);
}
else
{
SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
}
}
InvalidateControl();
}
See how this method is used to show a FailedAdorner
in the AdornerLayer
(if it's found and not null
). The last piece of the puzzle is to see what the actual FailedAdorner
looks like. Well, it's dead simple (there is a CustomAdornerBase
base class, but I'll spare you that); it looks like this:
using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Collections.ObjectModel;
namespace ThreadingComponent
{
public class FailedAdorner : CustomAdornerBase, IResizableAdornerControl
{
#region Data
private FailedUserControl failedUserControl;
#endregion
#region Constructor
public FailedAdorner(FrameworkElement adornedCtrl, String text)
: base(adornedCtrl)
{
failedUserControl = new FailedUserControl();
failedUserControl.ErrorMessage = text;
failedUserControl.Margin = new Thickness(0);
host.Children.Add(failedUserControl);
}
#endregion
#region IResizableAdornerControl
public void ResizeToFillAvailableSpace(Size availableSize)
{
host.Width = availableSize.Width - 5;
host.Height = availableSize.Height - 5;
failedUserControl.ResizeToFillAvailableSpace(availableSize);
}
#endregion
}
}
All it does is host a FailedUserControl
in the AdornerLayer
. So the last step is to see what the FailedUserControl
looks like. Well, here is its XAML:
<UserControl x:Class="ThreadingComponent.FailedUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid Margin="0,3,0,0" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Stretch"
Background="Black">
<Image Source="../Images/Failed.png"
Height="45" Margin="5"/>
<Label FontFamily="Arial" FontSize="24"
FontWeight="Bold" Content="Failed"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
Foreground="White"
VerticalAlignment="Center"
VerticalContentAlignment="Center"/>
</StackPanel>
<TextBox Grid.Row="1"
TextWrapping="Wrap" BorderThickness="0"
BorderBrush="Transparent" Background="Transparent"
IsReadOnly="True" FontSize="16"
FontWeight="Bold"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Text="The following error occurred whilst trying to obtain the data:"/>
<TextBox Grid.Row="2" TextWrapping="Wrap"
BorderThickness="0" Margin="0,10,0,0"
BorderBrush="Transparent"
Background="Transparent"
IsReadOnly="True"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Text="{Binding Path=ErrorMessage}"/>
</Grid>
</UserControl>
It can be seen that the FailedUserControl
uses the ErrorMessage
property to display the error that occurred with the threading operation. This ErrorMessage
property DP on the FailedUserControl
was set on the FailedUserControl
by the FailedAdorner
in response to the actual ThreadableItemViewModelBase.Failed
property changing.
Here is the code-behind for the FailedUserControl
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ThreadingComponent
{
public partial class FailedUserControl : UserControl, IResizableAdornerControl
{
#region Constructor
public FailedUserControl()
{
this.DataContext = this;
InitializeComponent();
}
#endregion
#region DPs
#region ErrorMessage
public static readonly DependencyProperty ErrorMessageProperty =
DependencyProperty.Register("ErrorMessage",
typeof(String), typeof(FailedUserControl),
new FrameworkPropertyMetadata(null));
public String ErrorMessage
{
get { return (String)GetValue(ErrorMessageProperty); }
set { SetValue(ErrorMessageProperty, value); }
}
#endregion
#endregion
#region IResizableAdornerControl
public void ResizeToFillAvailableSpace(System.Windows.Size availableSize)
{
this.Height = availableSize.Height;
this.Width = availableSize.Width;
}
#endregion
}
}
Note: ThreadableItemViewModelBase.IsBusy
works in the same way as this, except it has a BusyAdorner
and a BusyUserControl
.
As I have stated in various places in the article, the attached demo code's ThreadableHostControl
UserControl
uses the AdornerLayer
, which can occasionally come back as null
(say, if you are using your own AdornerDecorator
, or are in the middle of some crazy control such as an Infragistics one or a fat Ribbon), and as such, the attached demo code's ThreadableHostControl
UserControl
would not really work as planned.
To deal with this, the user can choose how this can be handled by using the App.Config setting "shouldThrowExceptionOnNullAdornerLayer
" which directs the app to throw an Exception if the AdornerLayer
can not be obtained.
If the user chooses to set the "shouldThrowExceptionOnNullAdornerLayer
" App.Config value to "false", the threading should all work as expected; it is just the busy or failed Adorners that will not be shown, and the user will have to work out another way of dealing with the IsBusy
and Failed
states of the background threading operation.
Obviously, it would be better for everyone if this setting remains set to "true" and the user finds out why the AdornerLayer.GetAdornerLayer(this)
is returning null
.
All you really have to do to use all this in your own app is:
- Copy the images to your Images/ folder; if you store images somewhere else, you will need to modify the
FailedUserControl
and BusyUserControl
.
- Copy the following files to your own app:
- BusyAdorner.cs
- CustomAdornerBase.cs
- FailedAdorner.cs
- IResizableAdorner.cs
- SimpleCommand.cs (if you want to use
ICommand
s)
- BusyUserControl.xaml/cs
- CircularProgressBar.xaml/cs
- FailedUserControl.xaml/cs
- ThreadableHostControl.xaml/cs
- Images folder: Put these where you like, but see Step 1
- BackgroundTaskManager.cs
- ThreadableItem.cs
- PropertyObserver.cs
- ThreadableItemViewModel.cs
- ThreadableItemViewModelBase.cs
- ViewModelBase.cs
- You can look at
Window1ViewModel
as a base, but you should create your own ViewModel to suit your needs
- App.config must contain the AppSettings key "
ShouldThrowExceptionOnNullAdornerLayer
"
- Create the right type of
ThreadableItemViewModel<T>
and expose that as a property from your ViewModel that drives your view, where T
is nailed down to some Type
- Create the correct
ThreadableItemViewModel<T>.TaskFunc
property value which is expected to be of type Func<Dictionary<String,Object>,ThreadableItem<T>>
There is no support for cancelling a threading work item delegate.
That is all I wanted to say right now. I have to say, for me, this was a real problem on a large scale WPF app, and this code will solve very real issues we have on that project. If you too feel that this could be useful in your WPF app, could you spare the time to make a comment or a vote? Many thanks.