.NET Asynchronous Events To Send Process Status To User Interface

By Robbe D. Morris

Printer Friendly Version

Robbe Morris
Robbe & Melisa Morris
  Download C# Source Code
These days I've been working on software prototypes for an upcoming research and analysis software family that spans across .NET Windows Forms, .NET Compact Framework, Macromedia Flash, Microsoft Excel, ASP.NET, .NET Web Services applications, and API's for external clients to program their applications against.  Fun stuff...  That said, I've really had to focus hard on my object oriented design throughout every facet of the process because almost every method or class would be used in more than one environment.  As an example, even the simple act of creating an HTML table in the ASP.NET application would need to be exposed directly to the .NET Windows Forms application or possibly an entirely different web site than our own.
Oh yeah, we need to have multi-lingual support no matter which environment the analysis model is executed in.
One of the first items we needed to address was how best to report progress of long running business layer methods back to the user interface.  We had to be very careful not to tangle up our business layer with references to classes in any one user interface layer.  Rather than re-invent the wheel, I went searching for best practices on this precise subject.  Unfortunately, all I found were bits and pieces but nothing that really addressed this topic specifically.  Thus, the reason for sharing this simple solution with you today.


The .NET Framework offers us the concept of raising events (and other items) in our classes asynchronously.  This means that we can raise the event in such a way as to not have the subscriber to that event (typically the user interface) hold up the processing in the method that raised the event.  The benefit being that it doesn't negatively impact the performance of our business layer method.
The first class in this sample is called ProcessStatus.Progress.  It is a fairly simple class that each of the business layer classes can inherit.  It provides support for raising events for up to two different processes in each business layer method.  Those events include:  Load, UnLoad, Item1CountCalculated, Item1Processing, Item2CountCalculated, and Item2Processing.
This technique allows us to push these process status events back up to the event subscriber without needing to know, or have a reference to, who the subscriber is.  Thus, we can use the same business layer anywhere in our software family.
The next step in the process was to begin creating environment specific reusable user interfaces to display the method progress.  The ReusableForms.ProgressWatcher is a class that inherits from System.Windows.Forms.Form.  You'll want to notice that it is in it's own class library.  Just because it is a windows form doesn't mean it has to be in a windows application .exe in order to be used.  This gives us a reusable interface to our UI progress bar form.  We have a slightly modified form that we use in our .NET Compact Framework assemblies not shown in the sample code.
The last step in the process was to create a class in each UI layer to instantiate the desired business layer class, wire up the events to our ReusableForms.ProgressWatcher class, execute the method(s), and react to the results.  Depending on your configuration, you may or may not want this in it's own assembly (particularly if you have multiple .NET Windows Forms applications that will call these same methods).
All that is left in each user interface form is two lines of code to call the business layer method and receive progress indicators.  One line to instantiate the class and one line to call the method itself.
I've included a sample Microsoft Access database for the sole purpose of creating a dummy business layer method that acts like a real application would while processing data.  Hopefully, you've found this tip helpful.  If you have questions or comments, please post them to our message board.
 
ProcessStatus.Progress.cs
 
 using System;
using System.Diagnostics; 

namespace ProcessStatus
{
 
      public class Progress
      {
	 
            public event System.EventHandler Item1CountCalculated;
            public event System.EventHandler Item2CountCalculated;
            public event System.EventHandler Item1Processing; 
            public event System.EventHandler Item2Processing; 
            public event System.EventHandler Load;
            public event System.EventHandler UnLoad;

            public string CompletionMessage = "";
            public string Item1Description = "";
            public int Item1Count = 0;
            public int Item1ProcessedCount = 0;
            public string Item2Description = "";
            public int Item2Count = 0;
            public int Item2ProcessedCount = 0;
            
            public void OnItem2Processing(string description)
            {
                if ( Item2Processing == null ) { return; }
                this.Item2Description = description;
                this.Item2ProcessedCount++; 
                Item2Processing.BeginInvoke( this, new EventArgs(),
                                             new AsyncCallback(Item2ProcessingCompleted),null);
            } 

            private void Item2ProcessingCompleted( IAsyncResult ar )
            {
                if ( Item2Processing == null ) { return; }
                Item2Processing.EndInvoke( ar );
            }
            
            public void OnItem1Processing(string description)
            {
                if ( Item1Processing == null ) { return; }
                this.Item1Description = description;
                this.Item1ProcessedCount++; 
                Item1Processing.BeginInvoke( this, new EventArgs(),
                                 new AsyncCallback(Item1ProcessingCompleted),null);
            } 

            private void Item1ProcessingCompleted( IAsyncResult ar )
            {
                if ( Item1Processing == null ) { return; }
                Item1Processing.EndInvoke( ar );
            }

            public void OnItem1CountCalculated(int count)
            {
       
                this.Item1Count = count;
                this.Item1ProcessedCount = 0;
                Item1CountCalculated.BeginInvoke( this, new EventArgs(),
                          new AsyncCallback(Item1CountCalculatedCompleted),null);
            } 

            private void Item1CountCalculatedCompleted( IAsyncResult ar )
            {
                if ( Item1CountCalculated == null ) { return; }
                Item1CountCalculated.EndInvoke( ar );
            }
            
            public void OnItem2CountCalculated(int count)
            {       
                this.Item2Count = count;
                this.Item2ProcessedCount = 0;
                Item2CountCalculated.BeginInvoke( this, new EventArgs(),
                           new AsyncCallback(Item2CountCalculatedCompleted),null);
            } 

            private void Item2CountCalculatedCompleted( IAsyncResult ar )
            {
                if ( Item2CountCalculated == null ) { return; }
                Item2CountCalculated.EndInvoke( ar );
            }
           
            public void OnLoad(string item1Description,string item2Description)
            {
                if ( Load == null ) { return; }
                this.Item1Description = item1Description;
                this.Item1Count = 0;
                this.Item1ProcessedCount = 0;
                this.Item2Description = item2Description; 
                this.Item2Count = 0;
                this.Item2ProcessedCount = 0;
                this.Load(this, new EventArgs());
                Load.BeginInvoke( this, new EventArgs(),
                     new AsyncCallback(LoadCompleted),null);
            } 
		
            private void LoadCompleted( IAsyncResult ar )
            {
                if ( Load == null ) { return; }
                Load.EndInvoke( ar );
            }
                        
            public void OnUnLoad(string completionMessage)
            {
                this.CompletionMessage = completionMessage;
                UnLoad.BeginInvoke( this, new EventArgs(),
                         new AsyncCallback(UnLoadCompleted),null);          
            } 
		
            private void UnLoadCompleted( IAsyncResult ar )
            {
                if ( UnLoad == null ) { return; }
                UnLoad.EndInvoke( ar );
            }
            
    }
}

 
BusinessLogic.cs
 
using System;
using System.Data;
using System.Data.OleDb;  
using System.Threading;

namespace BusinessLogic
{

   public class Sample : ProcessStatus.Progress
   {

     public Sample()
     {
 
     }

     public bool Method1(string connectionString,string someParameter)
     {
       this.OnLoad("First Step In Process Beginning",""); 
       Method2(connectionString);
       this.OnUnLoad("All done"); 
       return true;
     }

     private void Method2(string connectionString)
     {

        int maxtrys = 10;

        this.OnItem1CountCalculated(maxtrys); 

        for(int i=0;i<maxtrys;i++)
        {
           this.OnItem1Processing("main process " + i.ToString() + " of " + maxtrys.ToString()); 			  
           Method3(connectionString);
        }

     }

     private void Method3(string connectionString)
     {

        DataTable dt = new DataTable();
        int maxtrys=20;

        string sql = "select * from Hierarchy";

        try
        {

          this.OnItem2CountCalculated(maxtrys);
			
          for(int i=0;i<maxtrys;i++)
          {

            this.OnItem2Processing("sub process " + i.ToString() + " of " + maxtrys.ToString()); 
			 
            using(OleDbDataAdapter da = new OleDbDataAdapter(sql,connectionString))
            {
               da.Fill(dt);
            }
 	 
          }

        }
        catch (Exception) { throw; }

     }

  }
}
 
Sample.LongRunningMethods
 
using System;

namespace Sample
{

   public class LongRunningMethods : ReusableForms.ProgressWatcher 
   {
		  
      public void MyProcessThatTakesAwhile(string connectionString,string someKeyParameter) 
      {
             
         try
         {
			
           string desc = "";

           desc += "This is a really long description explaining some stuff ";
           desc += "regarding what in the world is transpiring while these neat ";
           desc += "progress bars move around.";

           this.DefineSettings(false,desc,"Preparing To Run...","");

           this.Show();
 
           BusinessLogic.Sample logic = new BusinessLogic.Sample();
 
           logic.Load += new System.EventHandler(this.Progress_OnLoad);
           logic.UnLoad += new System.EventHandler(this.Progress_OnUnLoad);
           logic.Item1CountCalculated += new System.EventHandler(this.Progress_OnItem1Start);
           logic.Item1Processing += new System.EventHandler(this.Progress_OnItem1Change);
           logic.Item2Processing += new System.EventHandler(this.Progress_OnItem2Change);
           logic.Item2CountCalculated  += new System.EventHandler(this.Progress_OnItem2Start);

           Hourglass(true);

           logic.Method1(connectionString,someKeyParameter);
 
        }
        catch (Exception err) { ShowError(err.Message); }
        finally {   Hourglass(false); }
            
   }


  }
}
 
ReusableForms.ProgressWatcher
 
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO;  
using System.Threading;

namespace ReusableForms
{
 
   public class ProgressWatcher : System.Windows.Forms.Form
   {

        protected bool CloseOnCompletion = false;
        private System.Windows.Forms.Label lblItem1Description;
        private System.Windows.Forms.Label lblItem2Description;
        private System.Windows.Forms.Label lblCompletionMessage;
        private System.Windows.Forms.Label lblTopBar;
        private System.Windows.Forms.Label lblDescription;
        private System.Windows.Forms.ProgressBar progressBar1;
        private System.Windows.Forms.ProgressBar progressBar2;
        private System.Windows.Forms.Button cmdClose;
        private System.ComponentModel.Container components = null;
        
        public ProgressWatcher()
        {
           InitializeComponent();
        }

        protected void DefineSettings(bool closeOnCompletion,
                               string summaryDescription,
                               string item1Description,
                               string item2Description)
        {
			 
          InitFormElements(closeOnCompletion,summaryDescription);
          this.lblItem1Description.Text = item1Description; 
          this.lblItem2Description.Text = item2Description;
            
        }

        protected void DefineSettings(bool closeOnCompletion,
                               string summaryDescription,
                               string item1Description)
        {		
            InitFormElements(closeOnCompletion,summaryDescription);
            this.lblItem1Description.Text = item1Description;
            this.lblItem2Description.Visible = false; 
            this.progressBar2.Visible = false; 
        }

 
        private void InitFormElements(bool closeOnCompletion,
                                      string summaryDescription)
        {
            this.lblCompletionMessage.Text = "";
            this.lblDescription.Text = summaryDescription;
            this.CloseOnCompletion = closeOnCompletion;   			 
        }
 
       
        protected void Progress_OnItem1Start(object sender, System.EventArgs e)
        {
 
           try
           {
             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;
             this.progressBar1.Value = 0; 
             this.progressBar1.Maximum = prg.Item1Count;  
             Application.DoEvents();
           }
           catch { }
        }

        protected void Progress_OnItem2Start(object sender, System.EventArgs e)
        {
 
           try
           {
             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;
             this.progressBar2.Value = 0; 
             this.progressBar2.Maximum = prg.Item2Count;  
             Application.DoEvents();
           }
           catch { }
        }

        protected void Progress_OnItem1Change(object sender, System.EventArgs e)
        {
 
           try
           {
             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;
             this.lblItem1Description.Text = prg.Item1Description;
             this.progressBar1.Value = prg.Item1ProcessedCount;  
             this.lblItem1Description.Refresh(); 
             Application.DoEvents();
           }
           catch { }
        }

        protected void Progress_OnItem2Change(object sender, System.EventArgs e)
        {
 
           try
           {
		    
             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;
             this.lblItem2Description.Text = prg.Item2Description;
             this.progressBar2.Value = prg.Item2ProcessedCount; 
             this.lblItem2Description.Refresh(); 
             Application.DoEvents();
           }
           catch { }
        }

        protected void Progress_OnLoad(object sender, System.EventArgs e)
        {
 
          try
          {

             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;

             if (prg.Item1Description.Length > 0)
             {
                this.lblItem1Description.Text = prg.Item1Description;
             }

             if (prg.Item2Description.Length > 0)
             {
                this.lblItem2Description.Text = prg.Item2Description;
             }

             Application.DoEvents();
          }
          catch { }
        }

        protected void Progress_OnUnLoad(object sender, System.EventArgs e)
        {
 
          try
          {
             ProcessStatus.Progress prg = (ProcessStatus.Progress)sender;
             this.lblCompletionMessage.Text = prg.CompletionMessage;
             this.cmdClose.Visible = true; 
             Application.DoEvents();
          }
          catch { }
        }

        protected void ShowError(string msg)
        {
          Hourglass(false);
          MessageBox.Show(msg);
        }

        protected void Hourglass(bool Show)
        {
           if (Show == true)
           {
              System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor;
           }
           else
           {
              System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.Default; 
           }
           return;
        }

        private void cmdClose_Click(object sender, System.EventArgs e)
        {
           this.Close();
        }

     
  }
}

 

Robbe has been a Microsoft MVP in C# since 2004.  He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.