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);