I’ve been having a bit of fun in the last few weeks developing the next major release of Time Clock MTS. Part of this release includes a system that sends out emails based on certain events occurring within the software. To accomplish this I could have included some POP email fields and asked users to enter their mail account details. Or I could have used MAPI email. But both of these approaches are ugly as they require input from the user and don’t work for web mail.
So I decided on an approach that goes something like this:
1. The existing application generates an XML file when an event email is required and saves it into the appropriate application data folder.
2. A process monitors the data folder and uploads the files to a web server as it finds them. Each upload process is executed on its’ own thread.
3. The web server processes the XML file and sends emails as required.
Step 1 was easy enough even in Visual Basic 6 (which Time Clock MTS is developed in). However step 2 was always going to be tough so I decided to build a small application in c# to perform this role. The VB6 application would start the C# app on startup and the C# app would shut itself down when the VB6 application closed. Step 3 would be accomplished easily enough with PHP. The key advantage (as I see it) with this approach is that the only requirement for a client computer is an internet connection. It doesn’t require an email client to be installed (like MAPI does) and doesn’t require the user to know and enter some arcane email server settings when they first install the program.
I won’t bore you with Step 1 or 3 but Step 2 had some interesting code that I thought I might share. The C# app is a standard Windows Forms app but I hide the form (except when the debugger is running). I could have made this a windows service but that seemed to introduce a level of complexity (and difficulty in debugging) that was unwarranted. Here’s how I hide the form in the form constructor:
InitializeComponent(); if (!Debugger.IsAttached) { // Prevent the window from showing up in the task bar AND when Alt-tabbing ShowInTaskbar = false; FormBorderStyle = FormBorderStyle.FixedToolWindow; // Move it off-screen StartPosition = FormStartPosition.Manual; Location = new Point(SystemInformation.VirtualScreen.Right + 10, SystemInformation.VirtualScreen.Bottom + 10); Size = new System.Drawing.Size(1, 1); }
The work that the form does is handled by a WebExtensionsService class that includes a couple of timers. The class is initialized in the form constructor and the timers are started at the same time. The first timer checks for the existence the parent VB6 program process. I’m sure there’s better ways of doing this but the method I used below works fairly well with little or no CPU overhead and raises an event.
private void timCheckForClose_Elapsed(object sender, ElapsedEventArgs e) { //writeLog("timCheckForClose update"); Process[] adminProcesses = Process.GetProcessesByName("appname"); if (adminProcesses.Count() == 0) { if (_bAppRunning == true) { _bAppRunning = false; OnAppNotRunning(EventArgs.Empty); } } else { if (!_bAppRunning ) { _bAppRunning = true; OnAppNotRunning(EventArgs.Empty); } } }
Then I could consume this event in the C# form with this:
private void WebExtensionsService_AppStopped(object sender, EventArgs e) { Application.Exit(); }
I used the Command Line Parser Library to read in a range of command line options that were passed to the C# application when it was started by the parent VB6 application. These settings were used by the second timer to do the real work of this C# forms app. Namely scanning a folder looking for XML files and then uploading them to a server. The second timer in the WebExtensionsService class does this:
private void timCheckFiles_Elapsed(object sender, ElapsedEventArgs e) { this.checkForXMLFiles(); } public void checkForXMLFiles() { Classes.Uploader uploader; if (uploaderThreads.Count < this.MaxThreads) { if (bDirectoryExists(this.XMLFolder)) { DirectoryInfo info = new DirectoryInfo(this.XMLFolder); FileInfo[] files = info.GetFiles().OrderBy(p => p.CreationTime).ToArray(); writeLog("checkForXMLFile:: " + files.Count().ToString() + " files found"); foreach (FileInfo file in files) { if (!uploaderThreads.ContainsKey(file.Name)) { uploader = new Classes.Uploader(); uploader.Filename = file.Name; Thread workerThread = new Thread(uploader.DoWork); uploaderThreads.Add(file.Name, workerThread); workerThread.Start(); writeLog("Starting uploader thread " + uploaderThreads.Count + " of " + this.MaxThreads); } if (uploaderThreads.Count >= this.MaxThreads) { writeLog("checkForXMLFile:: MaxThreads " + this.MaxThreads.ToString() + " are busy"); break; } } } else { writeLog("checkForXMLFile::"+this.XMLFolder+" doesn't exist"); } } else { writeLog("checkForXMLFile:: MaxThreads " + this.MaxThreads.ToString() + " are busy"); } }
The Uploader class is my worker class that is used to upload a file in it’s own thread. Note that I have a MaxThreads setting and maintain a collection of UploadedThreads to make sure I don’t try to upload the same file twice.
And for interests sake here’s the method from the Uploader class that does the actual work. It’s worth noting that the WebExtensionsService class is a singleton and contains all of the global settings I need. I’m sure this isn’t the most elegant way of doing things but it’s always worked extremely well for me in the past.
private bool uploadFile() { bool bReturn = false; string sResponse = "No Response"; WebClient wClient = new WebClient(); try { byte[] byte_response = wClient.UploadFile(WebExtensionsService.UploadLocation, "POST", WebExtensionsService.XMLFolder + "\\" + this.Filename); if (byte_response != null) { sResponse = System.Text.ASCIIEncoding.ASCII.GetString(byte_response); WebExtensionsService.writeLog("Upload " + this.Filename + " Reponse :: " + sResponse); if (sResponse == "1") { bReturn = true; } } } catch (Exception e) { WebExtensionsService.writeError(e); } if (bReturn) { sResult += " UPLOAD SUCCESSFUL"; } else { sResult += " UPLOAD FAILED RESPONSE " + sResponse; } return bReturn; }
Right now the uploadFile method waits for the correct response from the web server. I am thinking about not just uploading the file but also uploading a checksum of the file contents so that the web server can check that the file has not been corrupted in the upload process. But that’s something for another day.
The final step in the application is how to close it out cleanly. I showed earlier how the Application.exit() method was called when the parent VB6 program process was no longer present. Here’s the code that makes sure that all the uploader threads are closed out before the application exits.
private static void OnApplicationExit(object sender, EventArgs e) { Classes.WebExtensionsService WebExtensionsService; WebExtensionsService = Classes.WebExtensionsService.Instance; WebExtensionsService.writeLog("Closing TimeClockMTSWebExtensions"); foreach (KeyValuePair<string, System.Threading.Thread> entry in WebExtensionsService.UploaderThreads) //wait for uploaded threads to finish { System.Threading.Thread thread = entry.Value; thread.Join(); WebExtensionsService.writeLog("Waiting for " + entry.Key + " uploader thread to finish"); } WebExtensionsService.writeLog("Closed TimeClockMTSWebExtensions"); }
The key here is the thread.Join() call. This blocks the main C# program thread until the child threads (stored in my UploaderThreads collection) have completed their work.
Doing this work in C# is far easier than trying to do the same thing in VB6. The C# network code is about a million times easier and more robust. Part of me would LOVE to port the entire VB6 application to .NET but there’s three things stopping me. Firstly, the sheer amount of work. We’re talking about 80,000 lines of code that need porting. Not a trivial task. Second, ADO code in .NET simply isn’t as good as it is in VB6. It’s slower, handles connection pooling very poorly in comparison with VB6, and file sharing of a simple database like MS Access just doesn’t work as well. In the past I’ve had to perform tricky pre-caching of ADO data in .NET to get it performing even remotely as quickly as VB6 code. And finally, it scares me witless mainly because of all the reasons discussed in this great article by Joel Spolsky.
That being said stepping back to VB6 to do development work instead of using C# is like owning a 2013 model car and being forced to drive a leaky 1969 Volkswagen Beetle every second day. It works but it’s not fun. As a result I’m trying to build new features and move older non database reliant features (such as web camera image capture and network time checking code) into COM visible .NET code. However, because of the problems with database handling in .NET I don’t think I’ll ever get rid of VB6 altogether unless Windows drops compatibility for it.