Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / XML

Helper Class for Async

Rate me:
Please Sign up or sign in to vote.
4.97/5 (17 votes)
23 Aug 2017CPOL6 min read 14K   278   25   8
This is a generic class that makes it much easier to create another class in the background. It also provides a good sample of how to use the Task.Run with the ContinueWith clause.

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:

C#
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.

C#
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:

C#
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.

C#
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:

C#
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.

C#
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:

C#
class SecondaryViewModelAsyncWrapper1 : AsyncWrapper<SecondaryViewModel>
{
    public SecondaryViewModelAsyncWrapper1
            (Action<AsyncWrapper<SecondaryViewModel>> completedAction)
    {
        AbstractLoadAsync(() =>
            {
                completedAction?.Invoke(this);
            });
    }
}

This is the implementation that is in the sample:

C#
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:

C#
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:

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
QuestionAbout using SecondaryViewModelAsyncWrapper Pin
mr. Duan24-Aug-17 16:34
professionalmr. Duan24-Aug-17 16:34 
AnswerRe: About using SecondaryViewModelAsyncWrapper Pin
Clifford Nelson25-Aug-17 3:37
Clifford Nelson25-Aug-17 3:37 
GeneralMy vote of 5 Pin
mr. Duan24-Aug-17 16:26
professionalmr. Duan24-Aug-17 16:26 
AnswerRe: My vote of 5 Pin
Clifford Nelson25-Aug-17 3:35
Clifford Nelson25-Aug-17 3:35 
Questionsource code file link was broken Pin
Mou_kol23-Aug-17 22:30
Mou_kol23-Aug-17 22:30 
AnswerRe: source code file link was broken Pin
Clifford Nelson25-Aug-17 3:34
Clifford Nelson25-Aug-17 3:34 
Questiondownload link is broken!!!! Pin
Member 404996323-Aug-17 15:09
Member 404996323-Aug-17 15:09 
AnswerRe: download link is broken!!!! Pin
Clifford Nelson25-Aug-17 3:35
Clifford Nelson25-Aug-17 3:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.