Introduction
In many of Windows applications, it could be often necessary to use a time-consuming process (database query, remote function call, etc.). To give a feedback and in the best case a progress status of the operation to the user, it is reasonable to display a kind of message dialog, which closes automatically on the end of the time-consuming operation.
In some of my Projects, I used a kind of a splash dialog with a progress bar. The clou of this splash dialog: it is universal usable, for each kind of operation. It can be used as the welcome-screen as well as status dialog for a long term operation.
Using the delegates, the .NET-implementation of function pointers, makes the developing of an universal-purpose splash screen quite easy, passing the pointer to the time-consuming function as one of the parameters to it.
Background
The splash dialog is defined as a dialog form, with custom properties array of object type, also with a delegate function doing the job and the result property of object type. The delegate callback implements the Hollywood principle design pattern, useful for the GUI dialogs. The passed function is being executed as a BackgroundWorker
job, with WorkerSupportsCancellation and WorkerReportsProgress enabled.
This sample application uses the ContosoUniversity database and a part of the Data Access Layer library, taken from the ASP.NET article "Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application".
The time-consuming function is implemented here as a job gathering database items for a detailed student list. As there are very few records in the ContosoUniversity, additionally the working thread is being stopped for half a second in each iteration, to simulate a very long operation.
Additionally I used the PagedList NuGet package by Troy Goode.
The attached source code is a Microsoft Visual Studio 2013 C# project.
Using the code
The time-consuming function GetDetailedStudentList
has been defined in an own DataHelper
class:
public class DataHelper : IDisposable
{
private SchoolContext ctx = new SchoolContext();
public DataHelper()
{
ctx = new SchoolContext();
}
public PagedListResult<DetailedStudentInfo> GetDetailedStudentList(int size, int page, BackgroundWorker worker, DoWorkEventArgs e)
{
PagedListResult<DetailedStudentInfo> retValue = new PagedListResult<DetailedStudentInfo>();
try
{
int counter = 0;
int highestPercentageReached = 0;
retValue.Total = ctx.Students.Count();
var tmpList = ctx.Students
.OrderBy(o => o.LastName)
.ToPagedList(page, size)
.ToList()
.ConvertAll(new Converter<Student, DetailedStudentInfo>(
delegate(Student student)
{
return new DetailedStudentInfo(student);
}));
foreach (var student in tmpList)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
else
{
int perc = (int)((float)counter++*100/(float)retValue.Total);
if (perc > highestPercentageReached)
{
highestPercentageReached = perc;
worker.ReportProgress(perc);
}
student.StudentCourses = ctx.Enrollments
.Where(t => t.StudentID == student.StudentObject.ID)
.Select(c => c.Course).ToList();
foreach (var course in student.StudentCourses)
{
student.StudentInstructors.AddRange(course.Instructors);
}
}
Thread.Sleep(500);
}
retValue.ResultPagedList = tmpList;
}
catch (Exception)
{
e.Cancel = true;
}
return retValue;
}
public void Dispose()
{
if (ctx != null)
{
ctx.Dispose();
}
}
}
The SplashForm
is implemented as a standard Form with some custom properties defined.
using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace SplashDemo
{
public delegate object SplashCallBack(object[] state, BackgroundWorker worker, DoWorkEventArgs e);
public partial class SplashForm : Form
{
private readonly SplashCallBack _splashCallback;
private readonly object[] _stateParams;
public object ResultObject { get; private set; }
public Exception ExError { get; private set; }
public SplashForm()
{
InitializeComponent();
}
public SplashForm(SplashCallBack callback, object[] state, string title, string message)
: this()
{
this.Text = title;
this.TextBoxMessage.Text = message;
this._splashCallback = callback;
this._stateParams = state;
}
private void SplashForm_Shown(object sender, EventArgs e)
{
this.SplashProgressBar.Visible = true;
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
e.Result = this._splashCallback(_stateParams, worker, e);
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.SplashProgressBar.Value = e.ProgressPercentage;
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.SplashProgressBar.Visible = false;
if (e.Cancelled == true)
{
this.ExError = new Exception("Cancelled by User!");
this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
}
else if (e.Error != null)
{
this.ExError = e.Error;
this.DialogResult = System.Windows.Forms.DialogResult.Abort;
}
else
{
this.ResultObject = e.Result;
this.ExError = null;
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
this.Close();
}
private void ButtonCancel_Click(object sender, EventArgs e)
{
if (backgroundWorker1.WorkerSupportsCancellation == true)
{
backgroundWorker1.CancelAsync();
}
}
}
}
The point of the matter is the delegate definition defined in the namespace SplashDemo
and the custom properties _splashCallback
and _stateParams
. The result of the time-consuming function will be stored in the ResultObject
property. The occured error in the ExError
property.
public delegate object SplashCallBack(object[] state, BackgroundWorker worker, DoWorkEventArgs e);
public partial class SplashForm : Form
{
private readonly SplashCallBack _splashCallback;
private readonly object[] _stateParams;
public object ResultObject { get; private set; }
public Exception ExError { get; private set; }
The private properties as well as the title and message text are being set by the constructor overload.
public SplashForm(SplashCallBack callback, object[] state, string title, string message)
: this()
{
this.Text = title;
this.TextBoxMessage.Text = message;
this._splashCallback = callback;
this._stateParams = state;
}
The time-consuming function call is being initiated in the main form, by creating a SplashForm
instance, passing the function definition and required parameters as an array of objects - just by calling the overloaded SplashForm
constructor.
private void RefreshForm()
{
dataGridView1.DataSource = null;
this.dataGridView1.Refresh();
object[] stateParams = new object[] { this.PageSize, this.CurrentPage };
using (SplashForm splash = new SplashForm(RunSplashCallbackFunction,
stateParams, "Please wait", "Reading Data..."))
{
var dlgRetValue = splash.ShowDialog(this);
switch (dlgRetValue)
{
case DialogResult.OK:
var ret = (PagedListResult<DetailedStudentInfo>)splash.ResultObject;
this.StudentList = ret.ResultPagedList;
this.TotalPages = (int)Math.Ceiling((double)ret.Total / this.PageSize);
StatusLabel.Text = String.Format(
"Received a record list of {0} records. Displaying {1} rows of page {2}",
ret.Total,
ret.ResultPagedList.Count,
this.CurrentPage);
ReloadGridView();
break;
case DialogResult.Cancel:
StatusLabel.Text = "Operation cancelled by the user.";
break;
default:
StatusLabel.Text = String.Format(
"Error: {0}",
((Exception)splash.ExError).Message);
break;
}
}
}
private static object RunSplashCallbackFunction(object[] state, BackgroundWorker worker, DoWorkEventArgs e)
{
if (state.Length != 2)
throw new Exception("Parameter count mismatch!");
int page = (int)state[1];
int size = (int)state[0];
using (DataHelper dbh = new DataHelper())
{
return dbh.GetDetailedStudentList(size, page, worker, e);
}
}
The result can be retrieved after return by casting the ResultObject
property.
var ret = (PagedListResult<DetailedStudentInfo>)splash.ResultObject;
Other time consuming jobs can be executed the same way, just defining new functions matching the SplashCallback
delegate syntax. If you have no iterated steps in your time-consuming function, just change the SplashProgressBar
Style to Marquee.
Points of Interest
This ist an interesting example of using of delegate function and the Hollywood principle design pattern.
History
- 2014, 29th October: Initial release