Random programming things I'd want to remember

Monday, October 8, 2012

Responsive UI while uploading stuff in the background part 1


I was working on a program that required multiple uploads to be done in the background while updating the progress status for the user. I decided to go with WPF and C#. In this post, I will provide an outline for the program that simulates the upload without updating the interface. In the next posts I will explain how to update the UI and provide mechanisms to cancel uploads. Let's start with the interface:

<Window x:Class="BackgroundWorkerExperiment.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Button x:Name="startButton" Content="Start" Click="startButton_Click" Margin="5" />
        <Button x:Name="stopButton" Content="Stop" Click="stopButton_Click" Grid.Row="1" Margin="5" />
        <StackPanel Orientation="Horizontal" Grid.Row="2">
            <ProgressBar x:Name="progressBar1" Margin="5" Width="150" />
            <Label x:Name="overallStatusLabel" Margin="5" />
        </StackPanel>
        <StackPanel Orientation="Horizontal" Grid.Row="3">
            <ProgressBar x:Name="fileUploadProgressBar" Margin="5" Width="150" />
            <Label x:Name="currentFileStatusLabel" Margin="5" />
        </StackPanel>
    </Grid>
</Window>

Pretty simple form. Next, the class with helper methods that would "generate" some files:

    public static class Helpers
    {
        private static List<char> CharacterVocabulary_Get()
        {
            List<char> vocabulary = new List<char>();

            for (int i = 48; i <= 57; i++)
            {
                char a = (char)(i);
                vocabulary.Add(a);
            }

            for (int i = 64; i < 90; i++)
            {
                char a = (char)(i);
                vocabulary.Add(a);
            }
            for (int i = 97; i <= 122; i++)
            {
                char a = (char)(i);
                vocabulary.Add(a);
            }

            return vocabulary;
        }

        public static List<string> GetListOfNStringsOfKLength(int stringCount, int stringLength)
        {
            List<string> result = new List<string>();

            Random r = new Random();
            List<char> characters = CharacterVocabulary_Get();            

            for (int k = 1; k <= stringCount; k++)
            {
                StringBuilder sb = new StringBuilder(k.ToString(), stringLength+1);

                for (int j = 0; j < stringLength; j++)
                {
                    sb.Append(characters[r.Next(characters.Count)]);

                }
                result.Add(sb.ToString());
            }

            return result;
        }

        public static List<string> GetListOfNStringsOfVariableLength(int stringCount, int minStringLength, int maxStringLength)
        {
            List<string> result = new List<string>();

            Random r = new Random();
            List<char> characters = CharacterVocabulary_Get();

            for (int k = 1; k <= stringCount; k++)
            {
                int strLength = r.Next(minStringLength, maxStringLength);

                StringBuilder sb = new StringBuilder(k.ToString(), strLength+1);

                for (int j = 0; j < strLength; j++)
                {
                    sb.Append(characters[r.Next(characters.Count)]);

                }
                result.Add(sb.ToString());
            }
            return result;
        }

    }

Now, the class that does the simulated upload. It may look complex, but it has a lot of events that it raises before and after each of the meaningful states. First, the class signals that it starts the uploading process. Then, it signals that it starts uploading a certain file. Then, it signals that it starts uploading a part of that file. Then, after some time, it signals that it finished uploading the part of the file. The class also lets its user know that it finished uploading a file, and it finished uploading everything.

    public delegate void StartingUpload(object source, UploaderEventArgs e);
    public delegate void FinishedUpload(object source, UploaderEventArgs e);

    public delegate void StartingFileUpload(object source, FileUploaderEventArgs e);
    public delegate void FinishedFileUpload(object source, FileUploaderEventArgs e);

    public delegate void StartingFilePartUpload(object source, FilePartUploaderEventArgs e);
    public delegate void FinishedFilePartUpload(object source, FilePartUploaderEventArgs e);

    class Uploader
    {
        public List<string> FileNames { get; set; }
        public int DataToUpload_Length
        {
            get
            {
                int ret = 0;
                if (FileNames == null)
                    return ret;
                else
                {
                    foreach (string s in FileNames)
                    {
                        ret += s.Length;
                    }
                    return ret;
                }
            }
        }

        private int partsPerFile = 5;
        private long partUploadDelayNs = 10000;

        public event StartingUpload OnStartingUpload;
        public event FinishedUpload OnFinishedUpload;

        public event StartingFileUpload OnStartingFileUpload;
        public event FinishedFileUpload OnFinishedFileUpload;

        public event StartingFilePartUpload OnStartingFilePartUpload;
        public event FinishedFilePartUpload OnFinishedFilePartUpload;

        public Uploader()
        {
            FileNames = Helpers.GetListOfNStringsOfVariableLength(5, 8, 15);
        }

        public Uploader(List<string> fileNames)
        {
            FileNames = fileNames;
        }

        public void UploadFiles()
        {
            int partTracker = 0;

            if (OnStartingUpload != null) //Here I need number of pieces that will be uploaded
                OnStartingUpload(this, new UploaderEventArgs 
                {  
                    DataToUpload_Length = this.DataToUpload_Length, 
                    NumberOfFilesToUpload = FileNames.Count,
                    PartsToUpload_TotalCount = this.DataToUpload_Length,
                    PartsToUpload_UploadedSoFar = 0
                });

            for (int i = 0; i < FileNames.Count; i++)
            {
                //Log.UploadingFile(FileNames[i]);
                if (OnStartingFileUpload != null) //here I need the number of pieces in the file that will be uploaded, and file name
                    OnStartingFileUpload(this, new FileUploaderEventArgs { FileName = FileNames[i]/*, PartsInFile = FileNames[i].Length*/ });

                for (int k = 0; k < partsPerFile; k++)
                {
                    //Log.UploadingPartAOfB(k+1, partsPerFile);
                    if (OnStartingFilePartUpload != null) //here I need the number of pieces in the file that were uploaded so far and total number of pieces to upload
                        OnStartingFilePartUpload(this, new FilePartUploaderEventArgs { PartsUploadedSoFar = k, PartsToUpload = FileNames[i].Length, FileName = FileNames[i] });

                    Thread.Sleep(new TimeSpan(partUploadDelayNs*100));
                    
                    partTracker += 1;

                    //Log.UploadedPartAOfB(k+1, partsPerFile);
                    if (OnFinishedFilePartUpload != null) //here I need the number of pieces in the file that were uploaded so far and total number of pieces to upload
                        OnFinishedFilePartUpload(this, new FilePartUploaderEventArgs { PartsUploadedSoFar = k + 1, PartsToUpload = FileNames[i].Length, FileName = FileNames[i] });
                }

                if (OnFinishedFileUpload != null) //Here I need the file name
                     OnFinishedFileUpload(this, new FileUploaderEventArgs { FileName = FileNames[i]/*, PartsInFile = FileNames[i].Length*/ });
            }

            if (OnFinishedUpload != null) //Here I need the number of files uploaded
                OnFinishedUpload(this, new UploaderEventArgs { NumberOfFilesToUpload = FileNames.Count });
        }
    }

    public class FileUploaderEventArgs : EventArgs
    { 
        public string FileName { get; set; }
    }

    public class FilePartUploaderEventArgs : EventArgs
    {
        public int PartsUploadedSoFar { get; set; }
        public int PartsToUpload { get; set; }
        public string FileName { get; set; }
    }

    public class UploaderEventArgs : EventArgs
    {
        public int PartsToUpload_TotalCount { get; set; }
        public int PartsToUpload_UploadedSoFar { get; set; }

        public int DataToUpload_Length { get; set; }
        public int DataToUpload_Uploaded { get; set; }
        public int NumberOfFilesToUpload { get; set; }

        public string FileName { get; set; }
        public int PartsToUpload { get; set; }
        public int PartsUploaded { get; set; }

        public bool FileUpload { get; set; }
        public bool FilePartUpload { get; set; }
    }

And now it is time to look at the code behind the main form. Nothing fancy here either. Wire up the Uploader class, let the user know what's happening in the events that it raises, make sure the start button initializes the simulation.

    public partial class MainWindow : Window
    {
        Stopwatch s = new Stopwatch();
        Uploader u = new Uploader();

        public MainWindow()
        {
            InitializeComponent();
            InitializeUploaderEvents();
        }

        public void InitializeUploaderEvents()
        {
            u.OnStartingUpload += new StartingUpload(u_OnStartingUpload);
            u.OnFinishedUpload += new FinishedUpload(u_OnFinishedUpload);
            u.OnStartingFileUpload += new StartingFileUpload(u_OnStartingFileUpload);
            u.OnFinishedFileUpload += new FinishedFileUpload(u_OnFinishedFileUpload);
            u.OnStartingFilePartUpload += new StartingFilePartUpload(u_OnStartingFilePartUpload);
            u.OnFinishedFilePartUpload += new FinishedFilePartUpload(u_OnFinishedFilePartUpload);
        }
        
        void u_OnStartingFilePartUpload(object source, FilePartUploaderEventArgs e)
        {
            currentFileStatusLabel.Content = String.Format("Uploading file {0}, {1}/{2} parts done.", e.FileName, e.PartsUploadedSoFar, e.PartsToUpload);
        }

        void u_OnFinishedFilePartUpload(object source, FilePartUploaderEventArgs e)
        {
            currentFileStatusLabel.Content = String.Format("Uploading file {0}, {1}/{2} parts done.", e.FileName, e.PartsUploadedSoFar, e.PartsToUpload);
        }

        void u_OnStartingFileUpload(object source, FileUploaderEventArgs e)
        {
            currentFileStatusLabel.Content = String.Format("Starting to upload file {0}", e.FileName);
        }

        void u_OnFinishedFileUpload(object source, FileUploaderEventArgs e)
        {
            currentFileStatusLabel.Content = String.Format("Finished uploading file {0}", e.FileName);
        }

        private void startButton_Click(object sender, RoutedEventArgs e)
        {
            progressBar1.Maximum = u.DataToUpload_Length;
            progressBar1.Value = 0;
            u.UploadFiles();
        }

        void u_OnFinishedUpload(object source, UploaderEventArgs e)
        {
            s.Stop();
            overallStatusLabel.Content = String.Format("Finished uploading {0} files, elapsed time: {1}", e.NumberOfFilesToUpload, s.Elapsed);
        }

        void u_OnStartingUpload(object source, UploaderEventArgs e)
        {
            overallStatusLabel.Content = String.Format("Started uploading {0} files", e.NumberOfFilesToUpload);
            progressBar1.Maximum = e.PartsToUpload_TotalCount;
            progressBar1.Value = e.PartsToUpload_UploadedSoFar; //0
            s.Start();
        }

        private void stopButton_Click(object sender, RoutedEventArgs e)
        {

        }
    }

However, if you put this all together and hit the start button, there will be no feedback. The form will freeze and after a while it will unfreeze letting you know that the upload finished, but without letting you know what was happening during the upload process. Stay tuned on how to fix this.

No comments: