Introduction
I had a case where I needed to take a class that another developer was working on and put its creation on a background thread because it was taking too long for the application to startup. I initially did a non generic implementation, but later decided to create a generic implementation using a test bed project, which also provided a way of better testing that the concept was working. This was fortunate because I discovered a bug that would have been much harder to find if I had tried to test it in the true application instead of a test bed.
Background
The async
and await
keywords that were added to C# are very useful in the right circumstances, like working with events, but unfortunately not very useful for instantiating classes. It seems that I spend a lot of time working on attempting to get classes to instantiate faster since there is often a lot of processing associated with starting up an application, or initializing a class without using an event. It is also often much more convenient to put the start for a new window in the class itself, instead of in an event
handler.
The Design
The code for the async
helper class is as follows:
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace AsyncHelperSample
{
public class AsyncWrapper<T> where T : class
{
private T _item;
private Task loadTask;
private double _initializationMilliseconds;
public T Item
{
get
{
if (_item == null)
loadTask.Wait();
return _item;
}
set { _item = value; }
}
public double InitializationMilliseconds
{
get
{
if (_item == null)
loadTask.Wait();
return _initializationMilliseconds;
}
}
internal void AbstractLoadAsync(Action completeInitialization = null)
{
var startTime = DateTime.Now;
var constructor = typeof(T).GetConstructors().FirstOrDefault
(c => c.GetParameters().Length == 0);
Debug.Assert(constructor != null,
"The AbstractLoadAsync constructor without the constructor parameter requires "
+ "a parameterless constructor");
loadTask = Task.Run<T>(() =>
{
var item = constructor.Invoke(new object[] { });
return (T) item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
internal void AbstractLoadAsync(Func<T> constructor, Action completeInitialization = null)
{
var startTime = DateTime.Now;
loadTask = Task.Run<T>(() =>
{
var item = constructor();
return (T)item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
public void ExecuteAsync(Action<T> set)
{
if (_item == null)
Task.Run(() => loadTask.Wait())
.ContinueWith(result => set(Item));
else
set(Item);
}
}
}
There are two constructors for this class, one where there is no constructor of type Func<T>
parameter, and one where there is.
The AsyncWrapper
constructor where there is not a constructor parameter requires that the class have a default constructor. Reflection is used to find the default constructor, and if one is not found, there will be an exception raised. While in debug mode, a warning will be displayed if there is no default constructor. An issue with using the default constructor for a class
in that initialization information cannot be provided to the class before initialization. This may not be a serious issue since we can use services to get any data that the class
requires, and the Action<CompletedEventArgs<T>>
argument allows any values that can be changed after initialization to be made. The nice thing about using this constructor is that it is much more straight forward.
internal void AbstractLoadAsync(Action completeInitialization = null)
{
var startTime = DateTime.Now;
var constructor = typeof(T).GetConstructors().FirstOrDefault
(c => c.GetParameters().Length == 0);
Debug.Assert(constructor != null,
"The AbstractLoadAsync constructor without the constructor parameter requires "
+ "a parameterless constructor");
loadTask = Task.Run<T>(() =>
{
var item = constructor.Invoke(new object[] { });
return (T) item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
The use of this constructor is very straight forward:
public SecondaryViewModelAsyncWrapper
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
This is the constructor in a class derived from AsyncWrapper
class
, and has a parameter for the code instantiating the class to have an argument to be executed after the wrapped class has been initialized, and also has a subscription to the single event in the SecondaryViewModel
so that the code initializing this class can subscribe to an event
in the wrapped class
by simply subscribing to the event
in the wrapping class
. This way, the initializing code does not have to wait for the wrapped class
to be initialized. Use of the completeInitialization
parameter in calling the wrapping class
is optional.
For the AsyncWrapper
constructor where there is a constructor parameter, there is the additional complication of providing an Func<T>
that will return an instance of the class.
internal void AbstractLoadAsync(Func<T> constructor, Action completeInitialization = null)
{
var startTime = DateTime.Now;
loadTask = Task.Run<T>(() =>
{
var item = constructor();
return (T)item;
}).ContinueWith(result =>
{
Item = result.Result;
_initializationMilliseconds = DateTime.Now.Subtract(startTime).TotalMilliseconds;
completeInitialization();
});
}
This use of this constructor is slightly less straight forward:
var asyncViewModel = new SecondaryViewModelAsyncWrapper2(
() => new SecondaryViewModel("Scott"),
item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
One of the original reasons I have the Action<CompletedEventArgs<T>>
as an argument is that the derived class replicates any events in the class that was being created asynchronously. These events can be subscribed to in the AsyncWrapper
class
, and it would not be required to subscribe to those events in the original class
after it was created by the class
using the AsyncWrapper
. It also allows any other initialization to be done, like providing a pointer to the parent of the class
.
The other reason for this action is to provide the class using the AsyncWrapper
to be notified when upon the completion of initialization.
There is also an extra feature I added which probably is not required, but can be useful in a way to make changes in the wrapped class that will wait until the class has been initialized, but do it asynchronously.
public void ExecuteAsync(Action<T> set)
{
if (_item == null)
Task.Run(() => loadTask.Wait())
.ContinueWith(result => set(Item));
else
set(Item);
}
This method allows defining an Action
that will be executed after initialization of the class
has been completed, but can be defined at any time. If the class
has been initialized, then the Action
will execute immediately, otherwise it will wait, not pausing the thread, and wait for completion of initialization on another thread.
Creating the Derived Class
To use the AsyncWrapper
abstract
class
, a class
has to be created that inherits from this generic class
where the genetic Type
is the wrapped class. In the original design, I envisioned that each event of the wrapped class would be implemented in the wrapper, but this can be dispensed with since all that is required is to subscribe an event
in the Action
parameter of the AsyncWapper
constructor, which is executed after the initialization has completed. If the default constructor is available for the wrapped class, then the following would be the simplest implementation:
class SecondaryViewModelAsyncWrapper1 : AsyncWrapper<SecondaryViewModel>
{
public SecondaryViewModelAsyncWrapper1
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
completedAction?.Invoke(this);
});
}
}
This is the implementation that is in the sample:
class SecondaryViewModelAsyncWrapper1 : AsyncWrapper<SecondaryViewModel>
{
public event EventHandler<string> SendEvent;
public SecondaryViewModelAsyncWrapper1
(Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
private void SendEventHandler(object sender, string e)
{
SendEvent?.Invoke(sender, e);
}
}
I can see justification for both implementations, but think the first one is better, and only have left the old concept in place because some may see that adding each event
to the wrapper is preferred. There may also be other reasons.
The EventWrapper
derived class is created as follows when the default constructor can be used:
var asyncViewModel = new SecondaryViewModelAsyncWrapper1(item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
In this case, it can be seen that the wrapped class is assigned to the SecondaryViewModel
--this causes the raising of the PropertyChanged
event
that informs the View
that the SecondaryViewModel
property has been changed. Probably normally, this AsyncWrapper
would be used with the Model
instead of the ViewModel
, and this when the initialization of the Model
is complete then the properties of the ViewModel
are updated, and the PropertyChange
event
would be raised.
If the class
needs a constructor with parameters, which is the normal case, then the use wrapper class
would be like the following:
class SecondaryViewModelAsyncWrapper2 : AsyncWrapper<SecondaryViewModel>
{
public SecondaryViewModelAsyncWrapper2(Func<SecondaryViewModel> constructor,
Action<AsyncWrapper<SecondaryViewModel>> completedAction)
{
AbstractLoadAsync(
() =>
{
return constructor();
},
() =>
{
Item.SendEvent += SendEventHandler;
completedAction?.Invoke(this);
});
}
}
The wrapper class
constructor would need to have all the parameters necessary for the wrapped class
constructor plus a parameter for the Action
to execute when the initialization has completed.
The EventWrapper
derived class
is created as follows when the default constructor is not used:
var asyncViewModel = new SecondaryViewModelAsyncWrapper2(
() => new SecondaryViewModel("Scott"),
item =>
{
SecondaryViewModel = item.Item;
SecondaryViewModel.Milliseconds = item.InitializationMilliseconds;
});
The Sample
The sample has two buttons--one to create the class
asynchronously using the default constructor, and the bottom to create the class
with the constructor with the string
parameter. When a Button
is pressed, the instantiation of the class
begins and the controls are grayed out. To create a delay in the instantiation, a 5 second sleep is in the constructor. When the class
has been instantiated, the controls are no longer grayed out and the TextBox
shows the default name. In the case of the default constructor, this value is updated using the ExecuteAsync
method. With the constructor with the string
parameter, the value is the string
value passed to the constructor. Also, the time required to instantiate the class
is displayed below the TextBox
.
Initial View
After Button Pressed
Instantiation Completed
Conclusion
The same code could be used to allow a class to be instantiated internally, but using this separate class
eliminated the need for an event to be subscribed to indicate that instantiation is complete.
History
- 08/23/2017: Initial version