Cubicle Ninja

October 15, 2009

Etch-A-Sketch Emulator with the HTC Fuze Grav Sensor (G-Sensor)

Filed under: .Net, C# — Tags: , , , , , , , — Cubicle Ninja @ 6:17 pm

The premise behind this application was an etch-a-sketch style application I saw on a friend’s iPhone and a related comment they made about a missing feature…also, to be honest, my desire to prove that my Windows Mobile based phone could be just as “snazzy.”

Rather than trying to re-invent the wheel I set off of a grand adventure to track down an API for the HTC sensors (mainly the G-Sensor, GSensor, Grav Sensor or tilt sensor, depending on what you want to call it). The hunt began with a Google search for “htc fuze sensor api.” Additionally, my hunt ended with a Google search for “htc fuze sensor api.” On the first page of results I found a link to a blog post on Enterprise Mobile that appeared to be exactly what I needed (their information was greatly assisted by information from another blog that had reverse engineered the HTC dll that controlled the interaction with the sensors on the Fuze).

I played around for a bit and everything worked well but I had issues with the grav sensor not properly detecting when it was “at rest” on a level surface as well as some flakiness in other areas. I cracked open the source for the wrapper and decided to play around with how it returned the vectors from the sensor. After several tweaks and adjustments I settled on the following change:

Original code in HTCGSensor.cs:

        public override GVector GetGVector()
        {
            GVector ret = new GVector();
            HTCGSensorData data = GetRawSensorData();
            ret.X = data.TiltX;
            ret.Y = data.TiltY;
            ret.Z = data.TiltZ;
            // HTC's Sensor returns a vector which is around 1000 in length on average..
            // but it really depends on how the device is oriented.
            // When simply face up, my Diamond returns a vector of around 840 in length.
            // While face down, it returns a vector of around 1200 in length.
            // The vector direction is fairly accurate, however, the length is clearly not extremely precise.
            double htcScaleFactor = 1.0 / 1000.0 * 9.8;
            return ret.Scale(htcScaleFactor);
        }

New version I’m using:

        public override GVector GetGVector()
        {
            GVector ret = new GVector();
            HTCGSensorData data = GetRawSensorData();
            ret.X = Math.Round((data.TiltX/50.0), 1);
            ret.Y = Math.Round((data.TiltY/50.0), 1);
            ret.Z = Math.Round((data.TiltZ/50.0), 1);

            return ret;
        }

I round off the double value to 1 digit post decimal in order to keep the readings smooth. Using it this way I was able to get fluid-like smoothness in my sensor data as well as accurate readings for “at rest” in any position. I created a couple test apps with a little ball that could bounce off the walls and make the phone vibrate when a wall was hit at a certain velocity and so forth until I was satisfied that the sensor readings were going to provide me the results I was looking to get.

My next step was to create the tilt-to-draw application. To keep it simple I decided to just stick with good ol’ fashioned bitmap objects. The key things I wanted to achieve were:

  1. Selectable color from a palette
  2. Adjustable line thickness
  3. Smooth drawing with grav sensor
  4. Ability to save your drawing to the phone

And so I began by mocking up a quick layout that I would use
Tilt & Draw Design

I decided to make the tilt drawing aspect of it a toggle so that you could also draw with your finger / stylus if you so desired and settled on a slider for line thickness and 4 colors (red, blue, black and green). Before wiring up any of the controls I went ahead and did a test deploy to get an idea of how it was going to look and was dismayed at how the windows mobile task bar cluttered things up. First order of business…hide that task bar while the app is running!

To achieve this goal I needed to import to functions from coredll.dll and then make the proper calls within my initialization (and turn it back when the app was closing):

        [DllImport("coredll.dll")]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [DllImport("coredll.dll")]
        private static extern IntPtr ShowWindow(IntPtr hWnd, int visible);

        public SampleDrawing()
        {
            InitializeComponent();

            ShowWindow(FindWindow("HHTaskBar", null), 0); //0=hide, 1=show
        }

With that out of the way I began wiring everything up. I’ve attached the solution at the bottom of the post so I’m going to bypass listing out the generic code and just show the pertinent bits.

You’ll notice the reference to curPos in that function. I maintained two vector objects: curPos and lastPos to enable the smooth drawing of the lines around the screen. The variables were updated either by the “mouse” being moved around the screen or the grav sensor telling it the position needed to update (for this function I also used a check to keep the paintbrush within the bounds of the screen).

        private void SampleDrawing_MouseDown(object sender, MouseEventArgs e)
        {
            lastPos.X = curPos.X = ((MouseEventArgs)e).X;
            lastPos.Y = curPos.Y = ((MouseEventArgs)e).Y;
        }

        private void SampleDrawing_MouseMove(object sender, MouseEventArgs e)
        {
            if (!tiltEnable.Checked && allowDrawing)
            {
                curPos.X = e.X;
                curPos.Y = e.Y;
                drawGraphics();
            }
        }

        void CalculatePhysics(GVector gVector, int elapsedTime)
        {
            int maxX = ClientSize.Width - (int)myThickness;
            int maxY = ClientSize.Height - 45 - (int)myThickness;

            gVector = gVector.Scale(1 / friction);

            if (!(curPos.X <= 0 && gVector.X < 0) && !(curPos.X >= maxX && gVector.X > 0))
            {
                velocity.X = gVector.X * elapsedTime / 1000;
                curPos.X += velocity.X * elapsedTime / 1000;
            }
            if (!(curPos.Y <= 0 && gVector.Y < 0) && !(curPos.Y >= maxY && gVector.Y > 0))
            {
                velocity.Y = gVector.Y * elapsedTime / 1000;
                curPos.Y += velocity.Y * elapsedTime / 1000;
            }
        }

The actual drawing of the line happens the same regardless of how the cursor was moved and, to be honest, I had a bit of trouble determining an efficient method for drawing the line. I couldn’t just use the standard line methods because I wanted to be able to have variable thickness and that doesn’t mesh well with the normal line draw methods. To get around the issue of jaggy and gap-filled lines I opted to use the FillEllipse method. This gave the undesired effect of giving me variably sized dotted lines. My final solution uses a bit of generic math and a loop to draw the ellipses in a way that will create a “filled in” line. I’m certain there’s a better way to do this but darned if I was able to come up with it (Suggestions are VERY welcome).

        void drawGraphics()
        {
            // get tick count so we can calculate the time elapsed since last paint
            int thisPaint = Environment.TickCount;
            // put the vector into screen space
            if (tiltEnable.Checked)
            {
                GVector gvector = mySensor.GetGVector();
                gvector = gvector.Scale(htcDPI*friction);
                CalculatePhysics(gvector, thisPaint - lastPaint);
            }

            CalculateBounds();

            double rise = (lastPos.Y - curPos.Y);
            double run = (lastPos.X - curPos.X);
            double mult = (myThickness/2);

            if (Math.Abs(rise) > Math.Abs(run) && Math.Abs(rise) > mult)
                mult /= Math.Abs(rise);
            else if (Math.Abs(run) > mult)
                mult /= Math.Abs(run);

            rise *= mult;
            run *= mult;

            if (Math.Abs(run) > 0 || Math.Abs(rise) > 0)
            {
                while (((rise < 0 && lastPos.Y < curPos.Y) ||
                        (rise > 0 && lastPos.Y > curPos.Y)) ||
                       ((run < 0 && lastPos.X < curPos.X) ||
                        (run > 0 && lastPos.X > curPos.X))
                       )
                {
                    backBuffer.FillEllipse(myBrush, (int)lastPos.X, (int)lastPos.Y, (int)myThickness, (int)myThickness);
                    lastPos.X -= run;
                    lastPos.Y -= rise;
                }
            }
            lastPos = curPos;

            g.DrawImage(backBufferImage, 0, 0);

            lastPaint = thisPaint;
        }

This worked well enough to give me a smoothly flowing line on my Fuze and provided a decent-enough response time on things that it felt like you really were drawing as you tilted the screen around. At this point I had 3 of my 4 requirements completed and just needed the ability to save out the image. Luckily this ended up being trivial due to my choice of the bitmap object for drawing. I simply wired up the save button with the following code and was set.

        private void btnSave_Click(object sender, EventArgs e)
        {
            SaveFileDialog tmpDialog = new SaveFileDialog();
            tmpDialog.Filter = "Bitmap (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|PING (*.png)|*.png";

            if (tmpDialog.ShowDialog() == DialogResult.OK)
            {
                string tmpFileName = tmpDialog.FileName;
                ImageFormat tmpFormat = ImageFormat.Bmp;

                switch (tmpFileName.Substring(tmpFileName.Length - 4).ToLower())
                {
                    case ".bmp":
                        tmpFormat = ImageFormat.Bmp;
                        break;
                    case ".jpg":
                    case "jpeg":
                        tmpFormat = ImageFormat.Jpeg;
                        break;
                    case ".gif":
                        tmpFormat = ImageFormat.Gif;
                        break;
                    case ".png":
                        tmpFormat = ImageFormat.Png;
                        break;

                }
                backBufferImage.Save(tmpFileName, tmpFormat);
            }
            if (backBufferImage != null)
            {
                backBufferImage.Dispose();
                backBufferImage = null;
            }
            if (backBuffer != null)
            {
                backBuffer.Dispose();
                backBuffer = null;
            }
            backBufferImage = new Bitmap(ClientSize.Width, ClientSize.Height, PixelFormat.Format16bppRgb565);
            backBuffer = Graphics.FromImage(backBufferImage);
            backBuffer.Clear(Color.White);
            g.DrawImage(backBufferImage, 0, 0);
        }

I was fairly happy with the application but then the dreaded snarky comment was made: “Yeah, that’s neat, but you can’t turn it upside down and shake it to clear the screen like you can with a real etch-a-sketch.” I hate snark with a deep fiery passion…ok, that’s a lie I absolutely love it, and I also enjoy new ideas. I added in the feature of turning it upside down and “shaking it” to clear the screen. I put “shaking it” in quotes because, well, you don’t really have to shake it, you just have to turn it upside down (please don’t tell).

        void mySensor_OrientationChanged(IGSensor sender)
        {
            if (sender.Orientation == ScreenOrientation.FaceDown)
            {
                btnClear_Click(null, null);
                allowDrawing = false;
            }
            else if (!allowDrawing)
            {
                allowDrawing = true;
                curPos = lastPos = new GVector(ClientSize.Width / 2, ClientSize.Height / 2, 0);
                lastPaint = Environment.TickCount;
            }
        }

And there you have it. A cute windows mobile app that will let you draw around on the screen to your heart’s content and save out your beautiful creations to send as an MMS to friends and family the world over. As promised I’ve attached the entire solution for the project here. It includes my compiled version of the Sensors dll. If you’d like to get a copy of the DLL source code yourself you can grab it from the links at the top of the post and tweak it til you go blind.

Full Source Files

Exe and Dll for your phone

Mobile Installer (CAB)

October 7, 2009

Simple SQL Backups with SQL SMO

Filed under: .Net, C# — Tags: , , , , , , — Cubicle Ninja @ 9:56 am

    This post will show one of the methods of using Microsoft’s SQL SMO to create and restore database backups programatically.

    The basis behind the code in this post came from a need we had to create an automated process that would check three log files on our server to determine if our nightly imports had completed properly. If the logs all showed success then we created a backup of our three primary databases, otherwise we had to restore from the previous day’s backup.

    We had initially gone the route of doing this purely in SQL with scheduled jobs, but determined that we would have better control (and a larger base of support in our development team) if we created the utility to check the logs and work with the databases in .NET. Originally this entire process was completed using SQLDMO, however since that method is being phased out by Microsoft soon™, I have converted it over to use SMO.

    I’ve included the basics for backing up and restoring a database as well as the events that can be wired up should you have a need to do so.

The assemblies you need to include in your project are located in the following locations:

SQL Server 2005
\Program Files\Microsoft SQL Server\90\SDK\Assemblies
Include the Microsoft.SqlServer.Smo.dll assembly
Include the Microsoft.SqlServer.ConnectionInfo.dll assembly

SQL Server 2008
\Program Files\Microsoft SQL Server\100\SDK\Assemblies
Include the Microsoft.SqlServer.SmoExtended.dll assembly
Include the Microsoft.SqlServer.ConnectionInfoExtended.dll assembly

You’ll want to include the following using statements at the top of your project

using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;

    The following code is from a sample project I made as a C# Windows Form application. I added a simple label, progress bar and two buttons (one for backup and one for restore). The actual application we use is a console app, but this method is easier for demoing and explaining usage.

            Server dbServer = new Server(new ServerConnection("<SQL Server>", "<SQL User ID>", "<SQL User Password>"));
            Backup dbBackup = new Backup();
            /* The type of device you going to backup:  Database, Files or Log */
            dbBackup.Action = BackupActionType.Database;
            /* The SQL Database name you need to backup */
            dbBackup.Database = "<Database Name>";
            /* Keep in mind that the file path you use here is relative to the SQL Server, not your local file system */
            dbBackup.Devices.AddDevice(@"<Full Path Including File Name>", DeviceType.File);
            dbBackup.BackupSetName = "Automated Daily Backup Example";
            dbBackup.BackupSetDescription = "Sample database - Automated Daily Backup (Example)";
            /* This is an optional item that you can use to set when the backup data expires and
             * should no longer be considered "restorable"
            */

            //dbBackup.ExpirationDate = DateTime.Now.AddDays(7);

            /* Leaving the Initialize value set to the default of false will create
             * a new backup item as the last backup set.
             * If you change the Initialize value to true then this backup set will
             * become the first backup set and will overwrite other backups that have
             * the same BackupSetName
            */

            dbBackup.Initialize = true;

            /* Leaving the Incremental value set to the default of false will create
             * a full backup.
             * If you change the Incremental value to true it will only perform a
             * delta backup since the last full backup that was performed
            */

            //dbBackup.Incremental = false;
           
            /* There are several events that you can wire up to the Backup object
             * These will allow you to keep track of:
             *      The current progress of the backup
             *      When the current backup media is exhausted (full disk)
             *      When the current backup is completed
             *      When a message not captured by the above options is sent
            */

            dbBackup.PercentComplete += new PercentCompleteEventHandler(Event_PercentComplete);
            dbBackup.Complete += new ServerMessageEventHandler(Event_Complete);
            dbBackup.NextMedia += new ServerMessageEventHandler(Event_NextMedia);
            dbBackup.Information += new ServerMessageEventHandler(Event_Information);

            /* You can also use SqlBackupAsync with the same parameter if you want to run
             * asynchronously
            */

            try
            {
                dbBackup.SqlBackup(dbServer);
            }
            catch (Exception err)
            {
                responseLabel.Text = err.Message;
            }

    As you can see there’s not much to it. You just select your server, database, backup destination and then you can “fire and forget.” The events that are referenced above are all very generic and are mainly included just to show what is available with SQL SMO

        void Event_NextMedia(object sender, ServerMessageEventArgs e)
        {
            responseLabel.Text = e.Error.Message;
            responseLabel.ForeColor = Color.Red;
        }

        void Event_Information(object sender, ServerMessageEventArgs e)
        {
            responseLabel.Text = e.Error.Message;
            responseLabel.ForeColor = Color.Red;
        }

        void Event_Complete(object sender, ServerMessageEventArgs e)
        {
            responseLabel.Text = e.Error.Message;
            responseLabel.ForeColor = Color.Green;
        }

        void Event_PercentComplete(object sender, PercentCompleteEventArgs e)
        {
            dbProgress.Value = e.Percent;
            dbProgress.Update();
        }

    The restore process is equally simple from the SMO side of things, you just have to be careful due to the requirement of having exclusive access to the database prior to restoring it. If you aren’t in a production environment it is POSSIBLE to shutdown the SQL server then immediately run your restore after you start it back up. But I’ve included a method that I consider to be a little “cleaner.” (You may disagree and I’m always open to suggestions for improving it)

            Server dbServer = new Server(new ServerConnection("<SQL Server>", "<SQL User ID>", "<SQL User Password>"));
            Restore dbRestore = new Restore();
            dbRestore.Database = "<Database Name>";
            dbRestore.Action = RestoreActionType.Database;
            /* Keep in mind that the file path you use here is relative to the SQL Server, not your local file system */
            dbRestore.Devices.AddDevice(@"<Full Path Including File Name>", DeviceType.File);

            /* Leaving the ReplaceDatabase value set to the default of false will not create
             * a new database image so the database that is set must exist on the SQL server
             * If you change the Incremental value to true it will create a new image of the
             * database regardless of whether it currently exists or not
            */

            dbRestore.ReplaceDatabase = true;

            /* Leaving the NoRecovery value set to the default of false the tail end of the log
             * is backed up and the database will not be in a Restoring state
             * If you change the NoRecovery value to true then the database will be left in a
             * Restoring state.
             * This option is only used when the log is backed up as well
            */

            //dbRestore.NoRecovery = false;


            /* There are several events that you can wire up to the Restore object
             * These will allow you to keep track of:
             *      The current progress of the restore
             *      When the current backup media is exhausted (next disk)
             *      When the current backup is completed
             *      When a message not captured by the above options is sent
            */

            dbRestore.PercentComplete += new PercentCompleteEventHandler(Event_PercentComplete);
            dbRestore.Complete += new ServerMessageEventHandler(Event_Complete);
            dbRestore.NextMedia += new ServerMessageEventHandler(Event_NextMedia);
            dbRestore.Information += new ServerMessageEventHandler(Event_Information);

            /* You can also use SqlRestoreAsync with the same parameter if you want to run
             * asynchronously
             *
             ***************************** IMPORTANT NOTE ********************************
             * In order to perform a restore you must be able to acquire an exclusive lock
             * on the database or it will fail
             *
             * You can easily check to see what processes are currently running on the
             * database with the following command (alternatively you can use SP_WHO)
             *
             * SELECT spid
             * FROM master..SysProcesses
             * WHERE DBID IN (SELECT dbid FROM master..SysDatabases WHERE name = 'EPSS_PRD_EN')
             *
             * Once you have the list of the running processes against the DB you can use the
             * KILL procedure to terminate the session in order to free up the DB for restore
            */

            try
            {
                dbRestore.SqlRestore(dbServer);
            }
            catch (Exception err)
            {
                responseLabel.Text = err.Message;
            }

    As you can see in the comment above the call to the SqlRestore method (inconspicuously called out as an important note) you must have exclusive access to restore a database or it will throw an error and fail. I’ve read several different methods from various sources about ways to accomplish the exclusive access and finally settled on the one I’ve included here as the easiest and most reliable.

    A couple key things to remember is that the Service account for SQL Server has to be able to read / write to the folder you are using for the backup and restores. Hopefully it goes with out saying that another requirement is that the login you are using to the server has to have all of the proper rights to be able to backup and restore the database (sysadmin, db_owner).

    There are most likely other features of the SMO backup and restore that I’ve missed in this post, but I’ve just begun using it within the past week and am still ramping up my knowledge on it. So far I’ve not come across an instance where the above code (or a slightly altered version of it) has not been able to perform my required backup / restore operations (working on SQL 2008, 2005 and 2000).

September 30, 2009

Alternative to FileInfo for File…info

Filed under: .Net, VB — Tags: , , , , , , , — Cubicle Ninja @ 8:53 am

    This post will show how to import the FindFile utilities from kernel32 in order to improve application performance when you need to retrieve CreationTime, LastAccessTime or LastWriteTime information from a file.
 
My first attempt at a useful blog post…
 
    I recently had a need to create a tree view in an ASP .Net app that would function similarly to a standard windows explorer. After tweaking permissions I was able to get the app to impersonate a new user that was granted read permissions onto the network share and got everything running smoothly…then came the dreaded phone call “It’s great, we just need a few changes before go live.”

    The change seemed innocent enough, they just wanted a new column added into the tree view that would display the last modified time stamp for each file that was shown. A quick change to my GetFiles() loop to concatenate in the LastWriteTime property of the FileInfo object and I thought I was in business…not so much. The load time for the page went from sub 3 seconds to over 2 minutes. I tried everything I could think of: modifying when the nodes were populated, adjusting how they expanded, tweaking the buffer for the page, nothing seemed to work.

    After some searching around online I found the cause of the problem…FileInfo sucks if you want anything other than the most basic info (read: Name) of files in a directory (for me this included CreationTime, LastAccessTime and LastWriteTime). The general consensus was to utilize the FindFirstFile and FindNextFile methods from the kernel32.dll. Once I located that info it wasn’t that hard to convert my code over to make use of the imports, but I ran across a couple “gotchas” working with it in VB (my company mandated programming language, don’t mock me) and thought I’d share the final code I have implemented for anyone else that may have a need for something like this.

    First we define a couple Consts and Structs for use with the data we’ll be returning. This sets up the data to mesh with the FindFileData we’ll be working with from the dll imports.

    Public Const MAX_PATH As Integer = 260
    Public Const MAX_ALTERNATE As Integer = 14

    Public Structure FILETIME
        Public dwLowDateTime As UInteger
        Public dwHighDateTime As UInteger
    End Structure

' The CharSet must match the CharSet of the corresponding DllImport signature
   <System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, _
                                                 CharSet:=System.Runtime.InteropServices.CharSet.Unicode)> _
    Structure WIN32_FILE_DATA
        Public dwFileAttributes As UInteger
        Public ftCreationTime As FILETIME
        Public ftLastAccessTime As FILETIME
        Public ftLastWriteTime As FILETIME
        Public nFileSizeHigh As UInteger
        Public nFileSizeLow As UInteger
        Public dwReserved0 As UInteger
        Public dwReserved1 As UInteger
        <System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst:=MAX_PATH)> _
        Public cFileName As String
        <System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst:=MAX_ALTERNATE)> _
        Public cAlternateFileName As String
    End Structure

 

An important item to note is that on the WIN32_FILE_DATA structure, the CharSet must be the same as the CharSet on the DllImports or you’re going to have all kinds of difficulties reading back the data.
 

    Next are the actual imports. There are other methods that fit in with these for working with directory and file data, however I didn’t need / use them, so I’m not including them below.

    <System.Runtime.InteropServices.DllImport("kernel32", CharSet:=System.Runtime.InteropServices.CharSet.Unicode)> _
    Public Shared Function FindFirstFile(ByVal lpFileName As String, _
                                         ByRef lpFindFileData As WIN32_FILE_DATA) As IntPtr
    End Function

    <System.Runtime.InteropServices.DllImport("kernel32", CharSet:=System.Runtime.InteropServices.CharSet.Unicode)> _
    Public Shared Function FindNextFile(ByVal hFindFile As IntPtr, _
                                        ByRef lpFindFileData As WIN32_FILE_DATA) As Boolean
    End Function

 

    Now with that bit of code out of the way, making use of it is fairly straight forward, but admittedly more complex than a simple GetFiles() loop. In the below code is the function I created to load in the file info and to create the tree node as a link out to the page that will actually “render” the file as well as a function that is used to simply “clean up” the display name of the file.

    Function StripNulls(ByVal OriginalStr As String) As String
        If (InStr(OriginalStr, vbNullChar) > 0) Then
            OriginalStr = Left(OriginalStr, InStr(OriginalStr, vbNullChar) - 1)
        End If
        StripNulls = OriginalStr
    End Function

    ''' <summary>
   ''' Populates the TreeNode object with all of file data contained in the DirectoryInfo
   ''' </summary>
   ''' <param name="rootNode">The node to use as the parent for appending the file info</param>
   ''' <param name="rootDirectory">The directory that contains all of the files for this tree node</param>
   ''' <remarks></remarks>
   Protected Sub AddFiles(ByRef rootNode As TreeNode, ByRef rootDirectory As DirectoryInfo)
        Dim INVALID_HANDLE_VALUE As IntPtr = New IntPtr(-1)
        Dim FileName As String
        Dim hSearch As Long
        Dim WFD As WIN32_FILE_DATA

        hSearch = FindFirstFile(rootDirectory.FullName & "\*.pdf", WFD)
        If (hSearch <> INVALID_HANDLE_VALUE) Then
            Do While (FindNextFile(hSearch, WFD))
                Try
                    FileName = StripNulls(WFD.cFileName)
                    'This converts the Win32 File Data structures lastWriteTime to a human readable format
                   'DateTime.FromFileTime(((CType(WFD.ftLastWriteTime.dwHighDateTime, Long) << 32) + WFD.ftLastWriteTime.dwLowDateTime))
                   rootNode.ChildNodes.Add(New TreeNode("<span class='fileNameColumn'>" & FileName & "</span>" & _
                                                         "<span>" & DateTime.FromFileTime(((CType(WFD.ftLastWriteTime.dwHighDateTime, Long) << 32) + WFD.ftLastWriteTime.dwLowDateTime)) & _
                                                         "</span>", Nothing, "~/images/pdf.gif", "~/showPDF.aspx?displayName=" & _
                                                         Server.UrlEncode(FileName) & "&fileName=" & Server.UrlEncode(rootDirectory.FullName & "\" & FileName), "_blank"))
                Catch ex As Exception

                End Try
            Loop
        End If
    End Sub

 

    I had initially left in the GetFiles() loop for grabbing the .Name of the file and then used the imported functions to insert the last modified time stamp, but that proved troublesome due to the order of the files not always matching up between the two iteration methods.

    By incorporating this code in with my code that built out the directory tree nodes, I was able to provide the extra functionality the customer required and kept the load time at sub 3 seconds.

Powered by WordPress