Cubicle Ninja

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