Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An universal Desktop Splash Dialog with Progressbar

0.00/5 (No votes)
29 Oct 2014 1  
A simply universal Desktop Splash Dialog with a Delegate Function and Progressbar

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.

Main Form

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.

Data received

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
    {

        /// <summary>
        /// DbContext from ContosoUniversity
        /// </summary>
        private SchoolContext ctx = new SchoolContext();

        public DataHelper()
        {
            ctx = new SchoolContext();
        }

        /// <summary>
        /// The time-consuming function
        /// </summary>
        /// <param name="size">Page size</param>
        /// <param name="page">Current page number</param>
        /// <param name="worker">Backgroundworker instance for reporting to</param>
        /// <param name="e">Backgroundworker event to generate</param>
        /// <returns></returns>
        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)
                {
                    //check for Cancel button has been clicked
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        break;
                    }
                    else
                    {
                        //calculate current progress status
                        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);
                        }
                    }

                    //for simulating a very long operation
                    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
{

    /// <summary>
    /// The time-consuming function delegate
    /// </summary>
    /// <param name="state">The time-consuming function parameter array</param>
    /// <param name="worker">The calling worker object</param>
    /// <param name="e">The calling worker event</param>
    /// <returns></returns>
    public delegate object SplashCallBack(object[] state, BackgroundWorker worker, DoWorkEventArgs e);


    /// <summary>
    /// An universal Desktop Splash Dialog with Progressbar
    /// </summary>
    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();
        }


        /// <summary>
        /// Contructor overload
        /// </summary>
        /// <param name="callback">The time-consuming function call</param>
        /// <param name="state">The time-consuming function parameters</param>
        /// <param name="title">Dialog title</param>
        /// <param name="message">Text message to display</param>
        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;

            //call the callback function delegate
            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)
            {
                // Cancel the asynchronous operation.
                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.

        /// <summary>
        /// Execute the time-consuming function by SplashForm
        /// </summary>
        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;
                }
            }
        }

        /// <summary>
        /// The function to be referred as a delegate
        /// </summary>
        /// <param name="state">Parameter array</param>
        /// <returns>Object (as PagedListResult)</returns>
        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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here