views:

376

answers:

8

UPDATE: I pasted the entire source for Mr. Kraft. Having said that, this is probably my first real WinForms app, so please dissect and critique as well if you feel like it. Also, to make the critiquing less painful for me, I should note that I wrote this app very quickly, so I have a task of refactoring and improving design for it assigned to myself :)

So just a little background. I'm a .NET web guy but I needed to create a desktop app for our developers with some common tasks they do everyday to speed up productivity. The answer to this might be very obvious, but I've barely touched WinForms, so please bare with me.

I created a C# WinForms (.NET 2.0) app and everything works, but I have some issues. When I initially wrote it, the UI was feezing, so naturally I decided to create a thread to make things more fluid. This worked, however I can't debug the app because of this thread. The application chokes in VS.NET when I'm debugging it. VS.Net freezes. I keep reading about the UI thread, but have no idea how to access it and I don't really understand it aside from the fact that most likely anything UI related should be on this thread.

I've included a code snippet showing what I did to get the UI to stop from freezing.

using System;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Text.RegularExpressions;

namespace Acuity.Tools
{
    /// <summary>
    /// A form for managing Acuity development databases.
    /// </summary>
    public partial class DeveloperTools : Form
    {        
        #region Public Constructors
        public DeveloperTools()
        {
            InitializeComponent();
        }
        #endregion

        #region Private Constants
        private const string Scripts1To50Region = "#region 1 to 50#";
        private const string Scripts51AndUpRegion = "#region 51+#";                
        private const string DefaultSanitizedPassword = "t78";

        private const string UseSanitizedPasswordTemplateVariable = "%%UseSanitizedPassword%%";
        private const string DatabaseToRestoreTemplateVariable = "%%DatabaseToRestore%%";
        private const string DatabaseToRestoreToTemplateVariable = "%%DatabaseToRestoreTo%%";
        private const string SanitizedPasswordTemplateVariable = "%%sanitizedPassword%%";
        private const string StarKeyTemplateVariable = "%%StarKey%%";
        #endregion

        #region Private Methods
        /// <summary>
        /// Starts the database restore.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RestoreDatabaseButton_Click(object sender, EventArgs e)
        {
            RestoreDatabaseButton.Enabled = false;
            LogViewer.Text = string.Empty;
            UseWaitCursor = true;

            // Need to spawn a thread or else the UI freezes.
            Thread restoreDatabase = new Thread(new ThreadStart(RestoreDatabase));
            restoreDatabase.Start();
        }

        /// <summary>
        /// Starts the database restore.
        /// </summary>
        private void RestoreDatabase()
        {
            error = false;

            // TODO: Database restores fine, but it looks like an error is logged here. Not sure why.
            // for now I just ignore the errors. Will need to look into it.
            if (RestoreDatabaseCheckbox.Checked)
            {
                RestoreProductionDbToDevelopmentDb();
            }

            // TODO: The error reporting is a little buggy at the moment, It logs errors in a couple of cases even though
            // things work. Perhaps John can look at his dbobjects.vbs script at some point to see why an error is logged.
            if (!error)
            {
                if (GenerateDatabaseObjectsCheckbox.Checked)
                {
                    GenerateUpdatedDatabaseObjects();
                }

                if (!error)
                {
                    if (RunScriptsCheckbox.Checked)
                    {
                        RunVersioningScripts();
                    }
                }
            }

            RestoreCompleted();
        }

        /// <summary>
        /// 
        /// </summary>
        private void RestoreCompleted()
        {
            if (error)
            {
                MessageBox.Show("There were some errors!", "Error");
            }
            else
            {
                MessageBox.Show("Done!", "Done");
            }

            UseWaitCursor = false;
            RestoreDatabaseButton.Enabled = true;
            LogViewer.Enabled = true;
        }

        /// <summary>
        /// Runs versioning scripts on a development database.
        /// </summary>
        private void RunVersioningScripts()
        {
            LogViewer.Text += "Running versioning scripts..." + Environment.NewLine;

            FileInfo versioningScriptsBatchFileName = new FileInfo(BatchFileForVersioningScripts.Text);

            FileInfo scriptFileName = new FileInfo(applicationRootFolderPath + @"\Temp\" + (Guid.NewGuid()).ToString() + ".bat");

            StringBuilder fileContents = new StringBuilder();
            string drive = Regex.Match(versioningScriptsBatchFileName.Directory.FullName, "^[a-zA-Z]{1,1}").Value;
            fileContents.Append(drive + ":\n");
            fileContents.AppendFormat("cd \"{0}\"\n", versioningScriptsBatchFileName.Directory.FullName);
            fileContents.AppendFormat("\"{0}\" %1 %2 %3 %4 \"{1}\"\n", versioningScriptsBatchFileName.Name, applicationRootFolderPath + @"\Temp\DBObjects");

            File.WriteAllText(scriptFileName.FullName, fileContents.ToString());

            using (Process process = new Process())
            {
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.FileName = scriptFileName.FullName;
                process.StartInfo.Arguments = string.Format(
                        "{0} {1} {2} {3}",
                        SqlLogin.Text,
                        SqlPassword.Text,
                        DatabaseServer.Text,
                        DatabaseToRestoreTo.Text
                    );

                process.OutputDataReceived
                    += new DataReceivedEventHandler(OutputDataHandler);
                process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataHandler);
                process.Exited += new EventHandler(ProcessExited);
                process.EnableRaisingEvents = true;
                process.Start();
                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                while (!process.HasExited)
                {
                    Console.WriteLine("Still running");
                    Thread.Sleep(3000);
                }
            }

            scriptFileName.Delete();
            LogViewer.Text += "Finished running versioning scripts.";
        }

        /// <summary>
        /// Restores a production database to a development database.
        /// </summary>
        private void RestoreProductionDbToDevelopmentDb()
        {
            string restoreScriptTemplate = File.ReadAllText(applicationRootFolderPath + @"\Resources\RestoreProdDBToDevelopment.template");
            string restoreScript = restoreScriptTemplate.Replace(DatabaseToRestoreTemplateVariable, DatabaseToRestore.Text).Replace(DatabaseToRestoreToTemplateVariable, DatabaseToRestoreTo.Text);

            restoreScript = restoreScript.Replace(UseSanitizedPasswordTemplateVariable, UseSanitizePassword.Checked ? "1" : "0");

            if (UseSanitizePassword.Checked)
            {
                restoreScript = restoreScript.Replace(SanitizedPasswordTemplateVariable, SanitizedPassword.Text ?? DefaultSanitizedPassword);
            }

            restoreScript = restoreScript.Replace(StarKeyTemplateVariable, UseCustomStarKey.Checked ? StarKey.Text : DatabaseToRestore.Text + "Pwd");

            string restoreScriptFileName = applicationRootFolderPath + @"\Temp\" + (Guid.NewGuid()).ToString() + ".sql";

            File.WriteAllText(restoreScriptFileName, restoreScript);

            using (Process process = new Process())
            {
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.FileName = "sqlcmd";
                process.StartInfo.Arguments = string.Format(
                        "-U {0} -P {1} -S {2} -d {3} -i \"{4}\" -k -b",
                        SqlLogin.Text,
                        SqlPassword.Text,
                        "super-secret-database-server",
                        "master",
                        restoreScriptFileName
                    );

                process.OutputDataReceived
                    += new DataReceivedEventHandler(OutputDataHandler);
                process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataHandler);
                process.Exited += new EventHandler(ProcessExited);
                process.EnableRaisingEvents = true;
                process.Start();
                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                while (!process.HasExited)
                {
                    Console.WriteLine("Still running");
                    Thread.Sleep(3000);
                }
            }

            File.Delete(restoreScriptFileName);
        }

        /// <summary>
        /// Regenerates all database objects that will be updated in the development database.
        /// </summary>
        private void GenerateUpdatedDatabaseObjects()
        {
            LogViewer.Text += string.Format(
                   "Generating updated database objects from folder {0}",
                   DatabaseObjectsFolderPath.Text) + Environment.NewLine;

            FileInfo updateDatabaseObjectsScriptFileName = new FileInfo(applicationRootFolderPath + @"\Resources\dbobjscripts.vbs");
            FileInfo scriptFileName = new FileInfo(string.Format(@"{0}\Temp\{1}.bat", applicationRootFolderPath, Guid.NewGuid()));

            StringBuilder fileContents = new StringBuilder();
            string drive = Regex.Match(scriptFileName.Directory.FullName, "^[a-zA-Z]{1,1}").Value;
            fileContents.Append(drive + ":\n");
            fileContents.AppendFormat("cd \"{0}\"\n", scriptFileName.Directory.FullName);

            string dateToStartGeneratingFrom = string.Format("{0:yyyy/MM/dd}", GenerateDBObjectsDate.Value);

            LogViewer.Text += "Database objects will be updated starting from " + dateToStartGeneratingFrom;

            fileContents.AppendFormat("\"{0}\" {1} \"{2}\"\n", updateDatabaseObjectsScriptFileName.FullName, dateToStartGeneratingFrom, DatabaseObjectsFolderPath.Text);
            File.WriteAllText(scriptFileName.FullName, fileContents.ToString());

            using (Process process = new Process())
            {
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.FileName = scriptFileName.FullName;

                process.OutputDataReceived
                    += new DataReceivedEventHandler(OutputDataHandler);
                process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataHandler);
                process.Exited += new EventHandler(ProcessExited);
                process.EnableRaisingEvents = true;
                process.Start();
                Cursor.Current = Cursors.WaitCursor;
                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                while (!process.HasExited)
                {
                    Console.WriteLine("Still running");
                    Thread.Sleep(3000);
                }
            }

            scriptFileName.Delete();
            LogViewer.Text += "Finished generating updated database objects." + Environment.NewLine;
        }

        /// <summary>
        /// Raised when a database restore process has exited.
        /// </summary>
        /// <param name="sender">A <see cref="Process"/> that is exiting.</param>
        /// <param name="e">A <see cref="EventArgs"/>.</param>
        private void ProcessExited(object sender, EventArgs e)
        {
            Console.WriteLine("Exiting");
        }

        /// <summary>
        /// Catches the redirected error stream for Yui Compressor.
        /// </summary>
        /// <param name="sender">A <see cref="Process"/> that is currently executing.</param>
        /// <param name="e">A <see cref="DataReceivedEventArgs"/>.param>
        private void YuicErrorDataHandler(object sender, DataReceivedEventArgs e)
        {            
            if (!string.IsNullOrEmpty(e.Data))
            {
                error = true;
                CompressionLogViewer.Text += e.Data + Environment.NewLine;
            }
        }

        /// <summary>
        /// Catches the redirected error stream.
        /// </summary>
        /// <param name="sender">A <see cref="Process"/> that is currently executing.</param>
        /// <param name="e">A <see cref="DataReceivedEventArgs"/>.param>
        private void ErrorDataHandler(object sender, DataReceivedEventArgs e)
        {            
            if (!string.IsNullOrEmpty(e.Data))
            {
                error = true;
                LogViewer.Text += e.Data + Environment.NewLine;
            }
        }

        /// <summary>
        /// Catches the redirected output stream for Yui Compressor.
        /// </summary>
        /// <param name="sender">A <see cref="Process"/> that is currently executing.</param>
        /// <param name="e">A <see cref="DataReceivedEventArgs"/>.param>
        private void YuicOutputDataHandler(object sender, DataReceivedEventArgs e)
        {
            if (!string.IsNullOrEmpty(e.Data))
            {
                CompressionLogViewer.Text += e.Data + Environment.NewLine;
            }
        }

        /// <summary>
        /// Catches the redirected output stream.
        /// </summary>
        /// <param name="sender">A <see cref="Process"/> that is currently executing.</param>
        /// <param name="e">A <see cref="DataReceivedEventArgs"/>.param>
        private void OutputDataHandler(object sender, DataReceivedEventArgs e)
        {
            if (!string.IsNullOrEmpty(e.Data))
            {
                LogViewer.Text += e.Data + Environment.NewLine;

                if (e.Data.ToLower().Contains("login failed"))
                {
                    error = true;
                    return;
                }
            }
        }

        /// <summary>
        /// Closes the application via the File->Exit menu item.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ExitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        /// <summary>
        /// Raised when the Browse... button for selecting the database objects
        /// folder is clicked.
        /// </summary>
        /// <param name="sender">A <see cref="Button"/> control.</param>
        /// <param name="e">The <see cref="EventArgs" />.</param>
        private void DatabaseObjectsFolderPath_Click(object sender, EventArgs e)
        {
            SelectFolderDialog.ShowDialog();
            DatabaseObjectsFolderPath.Text = SelectFolderDialog.SelectedPath;
        }

        /// <summary>
        /// Raised when the Browse... button for selecting a batch file to run versioning scripts
        /// is clicked.
        /// </summary>
        /// <param name="sender">A <see cref="Button"/> control.</param>
        /// <param name="e">The <see cref="EventArgs" />.</param>
        private void BatchFileForVersioningScripts_Click(object sender, EventArgs e)
        {
            batchFileDialog.ShowDialog();
            BatchFileForVersioningScripts.Text = batchFileDialog.FileName;
        }

        /// <summary>
        /// Raised when the Run Versioning Scripts checkbox is checked/unchecked.
        /// </summary>
        /// <param name="sender">A <see cref="Checkbox" /> control.</param>
        /// <param name="e">The <see cref="EventArgs"/>.</param>
        private void RunScriptsCheckbox_CheckedChanged(object sender, EventArgs e)
        {
            DatabaseObjectsGroupBox.Enabled = RunScriptsCheckbox.Checked;
            SqlLogin.Enabled = RunScriptsCheckbox.Checked;
            SqlPassword.Enabled = RunScriptsCheckbox.Checked;
            DatabaseToRestoreTo.Enabled = RunScriptsCheckbox.Checked;
        }

        /// <summary>
        /// Raised when the Generate Database Objects checkbox is checked/unchecked.
        /// </summary>
        /// <param name="sender">A <see cref="Checkbox" /> control.</param>
        /// <param name="e">The <see cref="EventArgs"/>.</param>
        private void GenerateDatabaseObjectsCheckbox_CheckedChanged(object sender, EventArgs e)
        {
            BatchFileGroupBox.Enabled = GenerateDatabaseObjectsCheckbox.Checked;
        }

        /// <summary>
        /// Raised when the Restore Database checkbox is checked/unchecked.
        /// </summary>
        /// <param name="sender">A <see cref="Checkbox" /> control.</param>
        /// <param name="e">The <see cref="EventArgs"/>.</param>
        private void restoreDatabaseCheckbox_CheckedChanged(object sender, EventArgs e)
        {
            DatabaseToRestore.Enabled = RestoreDatabaseCheckbox.Checked;
        }

        /// <summary>
        /// Opens a log file to select a log file for analysis.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SelectLogFile_Click(object sender, EventArgs e)
        {
            openLogFileDialog.ShowDialog();
            AnalyzeLogFile(openLogFileDialog.FileName);
        }

        /// <summary>
        /// Analyzes a log file to see if there are any errors.
        /// </summary>
        /// <param name="fileName"></param>
        private void AnalyzeLogFile(string fileName)
        {
            if (!string.IsNullOrEmpty(fileName))
            {
                using (Stream file = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    if (file != null)
                    {
                        using (StreamReader sr = new StreamReader(file))
                        {
                            string text = sr.ReadToEnd();
                            sr.Close();

                            MatchCollection matches = errorLocator.Matches(text);
                            StringBuilder sb = new StringBuilder();

                            if (0 < matches.Count)
                            {
                                foreach (Match match in matches)
                                {
                                    sb.AppendLine(match.Groups["error"].Value);
                                }

                                ErrorsFound.Text = sb.ToString();
                            }
                            else
                            {
                                ErrorsFound.Text = "No errors found in log file!";
                            }
                        }

                        file.Close();
                    }
                }
            }
        }

        /// <summary>
        /// Reloads the error log.
        /// </summary>
        private void RefreshErrorLog()
        {
            AnalyzeLogFile(openLogFileDialog.FileName);
        }

        private void RefreshLogFile_Click(object sender, EventArgs e)
        {
            RefreshErrorLog();
        }

        private void BrowseFileToCompress_Click(object sender, EventArgs e)
        {
            selectJSOrCssFile.ShowDialog();
            FileToCompress.Text = selectJSOrCssFile.FileName;
        }

        private void StarUbcNickTesting(object sender, EventArgs e)
        {
            selectYuicFileName.ShowDialog();
            YuicFilePath.Text = selectYuicFileName.FileName;
        }

        private void BrowseFileToCompressTo_Click(object sender, EventArgs e)
        {
            selectJSOrCssFile.ShowDialog();
            FileToCompressTo.Text = selectJSOrCssFile.FileName;
        }

        private void GenerateCompressedFile_Click(object sender, EventArgs e)
        {
            Thread startCompression = new Thread(new ThreadStart(CompressedFile));
            startCompression.Start();
        }

        private void CompressedFile()
        {
            error = false;
            CompressionLogViewer.Text = "";

            if (string.IsNullOrEmpty(YuicFilePath.Text))
            {
                CompressionLogViewer.Text += "You need to specify the path to Yui Compressor" + Environment.NewLine;
                return;
            }

            if (string.IsNullOrEmpty(FileToCompress.Text))
            {
                CompressionLogViewer.Text += "You need to select a file to compress." + Environment.NewLine;
                return;
            }

            if (string.IsNullOrEmpty(FileToCompressTo.Text))
            {
                CompressionLogViewer.Text += "You need to select a file to compress to." + Environment.NewLine;
                return;
            }

            CompressionLogViewer.Text = string.Format(
                "Compressing file {0} to {1}",
                FileToCompress.Text,
                FileToCompressTo.Text
                ) + Environment.NewLine;

            using (Process process = new System.Diagnostics.Process())
            {
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.FileName = "java";
                process.StartInfo.Arguments = string.Format(
                    "-jar \"{0}\" -o \"{1}\" \"{2}\"",
                    YuicFilePath.Text,
                    FileToCompressTo.Text,
                    FileToCompress.Text
                    );

                process.OutputDataReceived
                    += new DataReceivedEventHandler(YuicOutputDataHandler);
                process.ErrorDataReceived += new DataReceivedEventHandler(YuicErrorDataHandler);
                process.Exited += new EventHandler(ProcessExited);
                process.EnableRaisingEvents = true;
                process.Start();
                Cursor.Current = Cursors.WaitCursor;
                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                while (!process.HasExited)
                {
                    Console.WriteLine("Still running");
                    Thread.Sleep(3000);
                }

                if (!error)
                {
                    CompressionLogViewer.Text += "The file has been compressed.";
                }
            }
        }

        private void BrowseForVersioningScriptsFolder_Click(object sender, EventArgs e)
        {
            SelectFolderDialog.ShowDialog();

            if (!string.IsNullOrEmpty(SelectFolderDialog.SelectedPath))
            {
                VersioningScriptsFolderPath.Text = SelectFolderDialog.SelectedPath;
            }

            BindFileTreeView();
        }

        private void BindFileTreeView()
        {
            if (!string.IsNullOrEmpty(VersioningScriptsFolderPath.Text) &&
                Directory.Exists(VersioningScriptsFolderPath.Text))
            {
                string[] versioningScriptFiles = Directory.GetFiles(VersioningScriptsFolderPath.Text, "*.sql");
                fileTreeView.Nodes.Clear();
                fileTreeView.CheckBoxes = true;
                SelectAll.Checked = true;

                foreach (string fullFileName in versioningScriptFiles)
                {
                    TreeNode node = new TreeNode(Path.GetFileName(fullFileName));
                    node.Checked = true;
                    node.SelectedImageKey = fullFileName;
                    fileTreeView.Nodes.Add(node);
                }
            }
        }

        private void GenerateVersioningScriptsBatchFile_Click(object sender, EventArgs e)
        {
            VersioningScriptsLogViewer.Text = string.Empty;

            if (string.IsNullOrEmpty(VersioningScriptsFolderPath.Text))
            {
                VersioningScriptsLogViewer.Text += "Please select a versioning scripts folder.";
                return;
            }

            DirectoryInfo versioningScriptFolder = new DirectoryInfo(VersioningScriptsFolderPath.Text);
            FileInfo versioningScriptsBatchFile = new FileInfo(string.Format(@"{0}\Update{1}.bat", versioningScriptFolder.FullName, versioningScriptFolder.Name));

            // Remove existing update batch file if any.
            if (versioningScriptsBatchFile.Exists)
            {
                versioningScriptsBatchFile.Delete();
                VersioningScriptsLogViewer.Text += "Removing existing versioning scripts batch file " + versioningScriptsBatchFile.FullName + Environment.NewLine;
            }

            VersioningScriptsLogViewer.Text += "Generating versioning scripts batch file " + versioningScriptsBatchFile.FullName + Environment.NewLine;

            script1To50 = new StringBuilder();
            script51AndUp = new StringBuilder();

            foreach (TreeNode versioningScript in fileTreeView.Nodes)
            {
                if (versioningScript.Checked)
                {
                    AddVersioningScriptToBatchFile(new FileInfo(versioningScript.SelectedImageKey));
                }
            }

            string templateFile = File.ReadAllText(applicationRootFolderPath + @"\Resources\VersioningScriptsBatchFile.template");
            string newBatchFileContents = templateFile.Replace(Scripts1To50Region, script1To50.ToString());
            newBatchFileContents = newBatchFileContents.Replace(Scripts51AndUpRegion, script51AndUp.ToString());

            File.WriteAllText(versioningScriptsBatchFile.FullName, newBatchFileContents);
A: 

How is your thread posting information back into the main thread? I've been able to debug multithreaded apps by placing breakpoints in the worker thread before.

Really, you ought to make the bit that runs in the seperate thread testable on it's own. That way you can debug it in isolation and not have to worry about the inter-thread issues.

Jon Cage
@Cage - true about making it testable. I whipped it up quickly, but will definitely do this.
nickyt
A: 

The UI (User Interface) thread is the main thread in your application responsible for processing windows messages, such as mouse clicks, keyboard input, repainting the screen etc.

So the UI thread is what calls RestoreDatabaseButton_Click (since this is a user interface interaction).

Before you introduced the new worker thread, this function was restoring a database from the UI thread. So while this was happening, the UI thread was not available to perform its other duties, such as redrawing the screen or responding to other user input, which was why your application appeared to freeze up.

The difficulties in debugging may just be because restoring a database is potentially a fairly intensive operation, involving writing to the file system which is a fairly low level task. The operating system may decide it needs to devote most of its time to the database restore, and not "context switch" very often, which is when the processor would switch to working on the UI thread. You could try starting the thread will a lower thread priority which should tell the OS to switch to your application more often.

Also, you need to be careful with how you manage the worker thread. For example, you should probably make it a member variable, and prevent the user from starting multiple worker threads concurrently (and disabling the restore button while the restore is happening). You should also consider providing a mechanism for stopping the thread should things go wrong, or when your application shuts down, to ensure the it shuts down cleanly.

John Sibly
+1  A: 

I think what you want to do here is send a message to the UI thread to take some action. You can pass this action has a delegate to the UI thread synchronously and asynchronously.

Basically, have your non-UI thread pull a reference to some UI object (any pre-existing instance of the Control class will do, perhaps your RestoreDatabaseButton instance) and call the Invoke method on it, passing a delegate containing the code to be executed as the parameter. This will enqueue a message on the UI thread to execute the delegate in its context rather than the context of your non-UI thread. The UI thread might already have a stack of messages to process, clicks, mouse movement, resize, etc., and will handle each in the order it was received. The Invoke method, will not return until the delegate you passed has been executed.

There's also a BeginInvoke method, this allows you to send a message to the UI thread that it will execute asynchronously. This method will return immediately, allowing your non-UI thread to continue executing even though the delegate may not have been executed yet. The EndInvoke method can be called after the BeginInvoke method and allows the non-UI thread to block until the delegate has been executed so that it can retrieve results from the delegate's execution. If the delegate doesn't return anything, there's no need to call this method.

In either of these ways, your non-UI thread can influence the UI thread and take actions against objects the live on it.

Travis Heseman
+2  A: 

Whenever you create an event handler for your form or controls it runs on the UI thread. The UI thread in a win forms app. is often also called the main application thread. This is the thread running the Application.Run() message loop.

I find it concerning that you say you "naturally decided to create a thread". Introducing extra threads is almost guaranteed to increase complexity and make your application harder to debug, as you have immediately discovered.

You have said you are inexperienced in non-web applications, so why introduce the complexity of threading on top of the learning curve of Windows Forms? Try a bit harder to get it working without using threads, then consider threads later on if it is still absolutely necessary.

In Windows forms you can often avoid threads by using a Timer and/or Application.DoEvents(). I am not saying don't use threads, just that I have seen many applications where it doesn't even make sense to allow the user to continue working while a process has not complete.

If you do want to stick with threads, I would also strongly recommend using the BackgroundWorker class. It nicely encapsulates a worker thread and provides good safe communication such as ProgressChanged and RunWorkerCompleted events.

Looking at your code I would also suggest doing some reading on accessing UI controls from worker threads. In short don't do it, unless using Control.Invoke() or Control.BeginInvoke(). This previous StackOverflow question would be a good start.

[UPDATE]

There are a number of places where your code is setting or checking properties of a UI control directly from the worker thread. To avoid exceptions and undefined behaviour you must always access UI controls from the thread that created them, ie. the UI/Main thread. Using Invoke() or BeginInvoke() allow you to do this safely.

Ash
I'm of mixed feeling about this answer. I like the explanation of what the UI thread is, but asking why a thread is introduced seems to take a step backwards. The short answer is that Windows requires it to keep the GUI responsive, as it uses the GUI thread to do things like window paints/redraws, operating the message pump, etc... See what Jon Skeet says here: http://stackoverflow.com/questions/1035439/database-access-in-gui-thread-bad-isnt-it/1035451#1035451
R. Bemrose
@R Bemrose, introducing threads to an application where they don't add some real benefit or there may be a viable alternatives is a BIG no-no. When you combine this with inexperience in Windows Forms this is a recipe for problems. Remember KISS and "the simplest thing that could work"?
Ash
@R Bemrose, in that answer Jon says about a non-responsive GUI: "In some cases, particularly for "quick and dirty" tools, that's actually the right choice". The real point is that if you use threads, your debugging, testing and maintenance complexity automatically increases.
Ash
+2  A: 

It's kind of hard to understand exactly what you're asking, but I just wanted to tell you about one little helpful thing when multithreading windows apps. In order to update any UI elements from another thread, you need to call invoke. Example:

Say you want to update a progress bar from a worker thread:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Thread thread = new Thread(DoSomeLongRunningTask);
        thread.Start();
    }

    private void UpdateProgressBar(int amount)
    {
        Action<int> action = (p) => { this.progressBar1.Increment(p); };
        this.Invoke(action,amount);
    }

    private void DoSomeLongRunningTask()
    {
        //do whatever....
        UpdateProgressBar(1);
    }
}

EDIT: My bad, I didn't realize you weren't working with C# 3.0. Here's the same code without the fancy anonymous method:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Thread thread = new Thread(DoSomeLongRunningTask);
        thread.Start();
    }

    delegate void ProgressBarIncrementer(int amount);

    private void UpdateProgressBarDelegateMethod(int amount)
    {
        this.progressBar1.Increment(amount);
    }

    private void UpdateProgressBar(int amount)
    {
        ProgressBarIncrementer incrementer = UpdateProgressBarDelegateMethod;
        this.Invoke(incrementer, amount);
    }

    private void DoSomeLongRunningTask()
    {
        //do whatever....
        UpdateProgressBar(1);
    }
}
BFree
+1 for using a anonymous function to update UI. That's kewl.
Steve
@BFree - Thanks but what you proposed doesn't work in .NET 2.0. I did stuff like this instead. Invoke((MethodInvoker)delegate { LogViewer.Text += "Running versioning scripts..." + Environment.NewLine; }); and it worked well. IS there a better way in .NET 2.0 to do this?
nickyt
See my edit. You need to create a delegate and then pass that in to the Invoke method.
BFree
+1  A: 

The UI thread is responsible for painting the graphical elements of an application on the screen as well as handling user input.

When developing a multithreaded Windows application, there really is one main rule:

Any code that accesses the UI must run in the UI thread.

This means that, for example, in order to update the status of a progress bar from a background thread you need to run the code that accesses the ProgressBar control object in the UI thread.

There are a couple of ways to achieve this. When using Windows Forms you can take advantage of the BackgroundWorker class.

This class allows you to run code in a background thread and access the UI whenever you need to update it with the status of the operation simply by handling some specific events. The BackgroundWorker then takes care of running the code in the appropriate thread for you behind the scenes.

Enrico Campidoglio
+1  A: 

In a couple of your methods, you're creating and starting a Process and then going into a While() loop with a Thread.Sleep(3000) call and waiting for the Process' HasExited property to return true.

A better way to do this is to add a handler for the Process' Exited event, and perform your clean-up code there. Since your methods are running on the UI thread, the While() loops are part of your problem.

MusiGenesis
A: 

Here's how I would approach this:

  1. Run all long running functions in individual Threads. You can accomplish this with the Thread class or a BackgroundWorker. Anytime I see a Sleep call, I would probably opt for a new Thread.

  2. Do the parameter validation in the UI thread. You are updating the "LogViewer" TextBoxes and returning on error. That's fine.

  3. Create a new Thread directly after the parameters are validated, the thread will call a function that handles spawning and waiting for the process.

  4. Report the feedback by calling Invoke on a function that updates the UI. See BFree's post on a cool way to call Invoke.

Steve