11 December 2007

A better http icon for your users

At the client we have been giving direct shortcuts to web application which sometimes require many different icons. The problem is that the default Windows icons for web shortcuts are very limited.

The solution is to point the icon variable to the shell32 dll, which will give you (and the user) a bit more variety.

The process is as follows:

1. Create the shortcut by right-clicking the desktop and choosing the location.
2. After naming and testing the shortcut, right-click on the shortcut and choose 'Properties'.
3. From the shortcut tab, push the 'Change icon' button.
4. In the browse window, browse to the following path: %SystemRoot%\system32\SHELL32.dll (or copy/paste the path given here).

Windows provides many more options from this dll.

Remove that pesky Windows Service

I have been writing a Windows Service (no easy feat, let me tell you) in Visual Studio and have found that getting it off the machine is just as difficult.

Following is a batch file to remove the service in question. The process is as follows: Use Add/Remove programs to get rid of the service, then run this batch file. One word of warning, the machine needs to be rebooted in order to remove the service.

VB Script File


On Error Resume Next

Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."
strKeyPath1 = "SYSTEM\ControlSet001\Services\[ServiceName]"
strKeyPath2 = "SYSTEM\ControlSet001\Services\Eventlog\Application\[ServiceName]"
strKeyPath3 = "SYSTEM\ControlSet002\Services\[ServiceName]"
strKeyPath4 = "SYSTEM\ControlSet002\Services\EventLog\Application\[ServiceName]"
strKeyPath5 = "SYSTEM\CurrentControlSet\Services\EventLog\Application\[ServiceName]"
strKeyPath6 = "SYSTEM\ControlSet001\Services\Eventlog\Application\[ServiceName].[ServiceName]"
strKeyPath7 = "SYSTEM\ControlSet002\Services\Eventlog\Application\[ServiceName].[ServiceName]"
strKeyPath8 = "SYSTEM\CurrentControlSet\Services\Eventlog\Application\[ServiceName].[ServiceName]"


Set objRegistry = GetObject("winmgmts:\\" & _
strComputer & "\root\default:StdRegProv")

DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath1
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath2
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath3
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath4
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath5
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath6
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath7
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath8

Set WSHShell = WScript.CreateObject("WScript.Shell")
WshShell.Run "C:\WINDOWS\system32\shutdown.exe -r -t 0"

Sub DeleteSubkeys(HKEY_LOCAL_MACHINE, strKeyPath)
objRegistry.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubkeys

If IsArray(arrSubkeys) Then
For Each strSubkey In arrSubkeys
DeleteSubkeys HKEY_LOCAL_MACHINE, strKeyPath & "\" & strSubkey
Next
End If

objRegistry.DeleteKey HKEY_LOCAL_MACHINE, strKeyPath
End Sub

26 October 2007

Things I learned about ASP.Net web services today

I have been writing a Web Service in C#/ASP.Net that manipulates PDFs, requires a long timeout period, and needs to perform some IO (moving files and cleaning up after itself). None of these things in themselves is difficult, but combined make for some hours of research.

Here are a few lessons I learned today, listed here for later use:

Lesson #1: File IO is not difficult to implement on a local XP machine, but can be tricky to get working. The application I am using uses the System.IO namespace, File.Move method, to rename files. When I first tried to get the application running, ASP kept returning the error 'Access denied to path.' A quick search around the net displays all kinds of answers, from editing the registry to granting IIS permission to the folder to adding the directory in question as a virtual directory.

For me, the answer turned out to be much more simple. My machine already had full access to the directories, so what was the source of the error. What I needed to do was add the identity tag to the Web.config file. The actual code is as follows (from an old MSDN article):

<configuration>
<system.web>
<identity impersonate='true'/>
</system.web>
</configuration>

I added this setting and then removed the permissions I had added before, and the File IO still worked on my machine. The implication is that the web service will impersonate the caller and have the same rights on the box as the rights of the calling application. Or anyway, that is what I imagine since the Web.Config documentation and my understanding of Windows security is lacking.

A long Microsoft explanation of impersonation can be found here.

Lesson #2: What happens if the service takes a long time to process?
If a service takes too long to process, the client will time out and you will either recieve an 'html/utf' error or a timeout error. The way to resolve this is as follows:
  1. In the client, add the System.Threading namespace to your calling class. When declaring your service variable, set the Timeout property of the Service to Timeout.Infinite, or the outer edge of what you expect the timeout to be in milliseconds. I usually use a calculation for millisecond settings, so I can quickly make the change. For instance, to set the Timeout property to 3 minutes, I would use myService.Timeout = 1000 * 60 * 3; 1000 milliseconds = 1 second, 60 seconds = 1 minute, for 3 minutes.

  2. The Web.config file will also need to be edited on the server. Use the httpRuntime tag and set the executionTimeout attribute to get the desired number of seconds in the timeout. No calculation is allowed as above. This tag also goes inside the system.web tag. Full Microsoft documentation of this tag can be found here.
I should mention that this application is an intranet application, so by definition all users have to be on a Windows network for these settings to be relevant to another environment.

23 October 2007

Some Useful Regular Expressions

I've been sifting through a lot of strings lately and experimenting with the C#/.Net RegularExpressions name space. I have been extracting the string values from PDF files and then using the strings to make database calls.

The challenge has been to clean up the strings as the PDF software I am using doesn't do any cleanup for me when transporting the string back to C#.

The expressions I have been using are as follows*:

Regular Expression: @"^[!""#$%&'()*+,-./:;?@[\\\]_`{|}~0-9]+"
Purpose: Remove non-alpha characters from the start of a string.

Regular Expression: @"[!""#$%&'()*+,-./:;?@[\\\]_`{|}~0-9]+$"
Purpose: Remove non-alpha characters from the end of a string.

Regular Expression: @"[^ -~’]"
Purpose: Remove unprintable characters from string.

Regular Expressions: "[^0-9]"
Purpose: Remove non-numerical characters from a string. (via AnimaOnline)


For a useful Regular Expression tool, check out the RegExLib.com site that has a convenient .Net tester.

* @ in front of string to mark it as a literal

18 October 2007

C#: Using foreach with an generic list (List<t>)

The List is a fantastic class for working with lists and arrays in C#, but it does not directly expose an iEnumerator to allow the foreach command to iterate through the collection. Until today, I would use the for...next structure to cycle through the collection to access each item.

The List also exposes a ForEach method which delegates the ForEach to another method. This is good for some applications that need to share the functionality - and while I understand the use from an OO perspective - can decrease the readability of the code.

There is a better way that can make the code readable and exposes the iEnumerable method.

The List method also exposes the ToArray() method. Use this method to access the items in the collection. For instance, if you have a List<t> called strings, you can write the code as follows:

foreach(string s in strings.ToArray()) { ...looping code here... }

24 September 2007

Enumerating the Enumerator

One of the joys of switching from Visual Basic to C# is the finding out about the power of the enumerator class. VB has fair enumerator support, but C# takes it to a whole new level with the enumerator class and all of it's methods.

Like VB, you can declare an enumerator and each value can have a custom numeric value. Like VB, you can declare and pass enumerator's as method arguments, which makes code very clear and easy to write.

But C# takes the enumerator many steps farther. First, the toString() method of the enumerator class prints out the actual string of the enumerator. This means that you can use it in string and numeric representations of the value.

One of the most valuable operations an enumerator class implements is the ability to inspect the class for membership. This helps make code more flexible because additions or changes to the enumerator class can flow through to other parts of the program.

For example, let's say you have a database enumerator that indicates what the database is doing at this time. This enumerator will be used for monitoring asynchronous calls to a database. The enumerator declaration would look like the following example.

Example 1: Sample Enumerator Declaration

public enum AsyncDBAction : int {
    insert,
    update,
    delete
}


Inspecting the enumerator object is very simple. In the example below, I use a foreach loop to iterate through the object and print individual member values to the console. The console will display the string representation and the numeric value for each enumerated member.

Example 2: Iterating through the Enumerator
foreach(AsyncDBAction val in Enum.GetValues(typeof(AsyncDBAction))) {
    Console.WriteLine("String value: " + val + "/numeric value " + (int)val);
}


How is this useful? If you are using the string value and the numeric value (or enumerated value), adding a new enumerated value such as 'select' or 'other' will not break your code. Simply add the new value to the enumerator and then examine it where needed to continue processing.

20 September 2007

Text search in SQL Server Procs/Views

In SQL Server 2000, sometimes one needs to find all the dependencies of one object on another. I don't trust the dependency functionality in SQL Server, so I wrote a query to scour the syscomments table in any database.

The following query allows you to search for text within queries and views in SQL Server. Just open Query Analyzer and paste the following code, replace the search string with the object name, and run.



SELECT [name]
FROM [dbo].[sysobjects] obj
INNER JOIN [dbo].[syscomments] cmt
ON obj.[id] = cmt.[id]
where cmt.[text] like '%search string%'

07 September 2007

Documenting Code in Visual Studio.Net - Part I

After writing that incredible .Net program comes the questions about support and documentation. Some people like Visio and other pictorial views of the documentation. I like external tools too...but I have always been an advocate of keeping the code and documentation together. What if the code changes? How do you update the documentation and keep everything current?

And then there is a question of style and standards. How do you enforce documentation standards for multiple developers...assuming you can get them to document in the first place. And what about the clutter that happens when people start writing all the comments within the code?

Automated Documentation in VS.Net
Luckily, Visual Studio.Net creates a standard method for documenting code. It creates a standard that is easy to stick to and I believe it even promotes more documentation*. The way to document code is to go to a class or method definition and type /// immediately above the definition of choice. VS.Net will then automatically add some commented XML to the file in question. If you are writing comments for a method with arguments, VS.Net will also creates for the arguments. If the method returns a value, an XML tag will be created for the XML value.

For example, let's say you have the following method:

public DataTable SelectInner(string TableName, string RelationName) {...}

Typing 3 slashes above the call, will automatically produce the following XML:

/// <summary>
///
/// </summary>
/// <param name="TableName"></param>
/// <param name="RelationName"></param>
/// <returns></returns>

To document the method, simply type your comments within the tags. The 'summary' tag defines the method, the 'param' tags define each parameter, and the 'returns' tag defines the return value. It's simple and give the user a quick place to define all the key inputs and outputs of the method.

Documenting code that is internal to the method is similar, but more free form. Simply type /// above the line or variable in question. VS.Net does not create fields, but these comments become more important in the next step, which is generating human readable documentation from the source files.

MSDN has documented their recognized tags. For further reading, check out this article, entitled 'XML Comments Let You Build Documentation Directly From Your Visual Studio .NET Source Files'.

-------------
* This posting applies to Visual C#/Visual C++ only. While there is theoretical support for J# and VB.Net, I have not personally tested it and therefore cannot vouch for how well it works.

30 July 2007

Limit a tag list in del.icio.us

There is a great way to send commands to del.icio.us by using the appending the setminposts query string to the end of the URL.

This will limit the number of tags that come up on the page...which is very useful when you are looking at the del.icio.us page from a PDA.

22 June 2007

Deploying files with an Install project

Microsoft Visual Studio Pro provides a suite of tools for deploying files and creating a folder structure at install. At this point, I am not an expert at the deployment (see other deployment postings), but following is how to deploy files:

1. Right-click on the Install/Deployment package
2. Choose View > File System
3. Add files and folders. They will be added to the install structure defined in the deployment package.

Deploying files with a ClickOnce Project

Deploying files with a ClickOnce Project

The switch to deploying a Windows Services has got me thinking about how to deploy files and settings with the application. So, today I have been exploring the various ways to deploy data files with C# projects. Following are the ways I have discovered:

--------------------------------------------------
Project Properties > Resources
Resources are files that can be compile directly into the executable. I would recommend this for XSD or XML files that will not be edited after deployment.

How to Reference:
[Namespace].Resources.Resource Name;

Pros: Easy to reference and deploy. In testing, XML files were rendered as strings and easily loaded into memory for easy parsing.
Cons: I do not believe that resources can be edited after deployment. They are compiled directly into the executable.
--------------------------------------------------
Project Properties > Settings
This looks like the best place to store editable data. These settings are deployed in the .config file stored in the application directory with the executable. The file is an XML file - change the appropriate setting. It looks like settings can be set to simple or complex types and can be changed programmatically at run time. They can also be changed at run time.

How to reference: [Namespace].Properties.Settings.Default[string id];

Pros: It is easy to change the settings once the application is deployed.
Cons: The format my take complex types, but I'm not sure if it is serialized in a human readable format. Strings are easily editable provided you can find the .config file on the hard drive.

--------------------------------------------------
Data Files
Data files are added directly to the project. They are deployed in their original state with each install.

How to reference: You need to check to see if the system is in a deployed state. If yes, the look in the ApplicationDeployment.CurrentDeployment.DataDirecrory. From an editing perspective, the files can be edited at runtime.

Pros: The file makes the transition to the deployed computer completely intact. Coding for the local location is not difficult.
Cons: The install directory for the data file is complex and contains a guid. If there is more than one version of the application installed on the machine, it may be difficult, if not impossible, to find and update the correct file. The coding for checking the deployed stated is a little obtuse, but that could be solved with a quick object.

Regular Expressions and Like Statement

There is a new requirement for this application to be able to parse numbers embedded in file names. One issue is that C# DataTables do not allow for complex, regular expression searches in fields. That means that I will have to create on the fly regular expressions, query my local files database and then run a post process on the remaining records to see if more than one file comes up.

The query process will be as follows:

1. When loading hooks into the table, replace [snam] with the SNAM
2. Create a boolean value in the table to indicate if the hook has a [n] wildcard in it
3. Query the files table with the first [n] in the hook as the */% wildcard
4. Pass the records to a function with the hook. The function returns the number of records found.
5. Build a regular expression based on the string. Replace '[n]' with '[0-9]+'
6. Cycle through the array. If a match is found, increment the match.
7. Return the array back to the calling function for processing.

14 June 2007

How to: Install and Uninstall Services

This program says you have to use the command line program installutil on the executable in order to get a Windows Service installed on the machine. The link above also discusses how to uninstall .Net services if desired.

13 June 2007

Writing Your Own Windows Service in Visual Studio

This entry is indebted to the following articles: MSDN: Walkthrough: Creating a Windows Service Application in the Component Designer and 15 Seconds: Creating an Extensible Windows Service.

The method for writing a Windows Service in Visual Studio is neither straightforward or easy. The main issue is that Windows does not allow Windows Services to display any type of UI or debugging at all. You are required to write your own logging routines in order to get feedback on where and how the application is performing. Following is my first stab at outlining the steps for successfully writing and deploying Windows Services.

Please note, Windows Services can only be written in Visual Studio Pro (or better). The 15 Seconds article talks about manually writing an installer, but the code does not compile. That's ok with me...VS Pro is available, why not use it?

The MSDN provides all the steps needed to write and deploy your service, although it is a little light on the details of why you need to take certain steps. There is something in me that doesn't like to paint-by-numbers. I always like to know the 'why', but I have to start today with the 'How'.

The steps are as follows:

1. Write and debug your program. Make sure you have a logging mechanism and your try/catch blocks are very tight and catch all potential errors and/or provide meaningful logs.

2. Per the MSDN article, add the service to your project. Add a timer object to the project and register your method with the Timer.Elapsed event. An excellent outline of how to write this code is found at the end of the 15 Seconds article above. Pay attention to how to start and stop the timer events, unless you are writing a multi-threaded application.

3. Create a new Setup project. Add your improved program with service to this new Setup project. Set up the file system, packaged files and project as you would like it to be deployed.

4. Right-click on the Setup project node. Select Build. An MSI file (with wizard) will be sent to the debug folder of the Setup project. This MSI file provides a wizard to install your service and create all the files necessary to run the application.

02 June 2007

How to use the Settings class in C# - The Code Project - C# Programming

This article updates my knowledge of application settings in Visual Studio. The important points are:

* Settings can be directly accessed using early binding. The way to access it remains:

1. Set the reference to the proper namespace:
using [Assembly Name].Properties;
2. Refer to the property (substitute property name):
Settings.Default.PropertyName
3. Save the property:
Settings.Default.Save();

* Settings as the Application level are read only. Settings at the User level are read/write.

02 February 2007

FSO Wrapper Classes - Comments

I wrote the FSO classes because I needed to have some late binding file capabilities but was tired of trying to guess all the method calls. There wasn't a requirement to implement all of the FSO functionality, so if you use these classes you may have to add a method or two for your own programming purposes.

Speed is an issue here for some larger file processing. If you need something speedy, I early-bind to the FSO objects or late bind directly in your code.

clsFSOSearch - VBA

Option Explicit

' a search class to go with the other FSO wrappers
'

Private col As VBA.Collection

' *****************************************************************************
Private Sub Class_Terminate()

Dim lngItem As Long

For lngItem = 1 To col.Count
col.Remove 1
Next lngItem

Set col = Nothing

End Sub

' *****************************************************************************
Private Sub Class_Initialize()
Set col = New VBA.Collection
End Sub

' *****************************************************************************
Public Function Item(Index As Variant) As clsFSOFile
Set Item = col.Item(Index)
End Function

' *****************************************************************************
Public Function Count() As Long
Count = col.Count
End Function

' ******************************************************************************
Public Function ClearResults() As clsFSOSearch
Call Class_Terminate
Call Class_Initialize

Set ClearResults = Me

End Function

' *****************************************************************************
Public Function Search(LookInFolder As String, _
SearchString As String, Optional ExclusionSearch As Boolean = False) _
As clsFSOSearch
' LookInFolder: Folder to search
' SearchString: Pattern to search for. Accepts '*' as wildcard
' ExclusionSearch: Excludes found files, add those that do not match

Dim strSearch() As String
Dim lngItem As Long

Dim objFile As clsFSOFile
Dim objFolder As clsFSOFolder

strSearch() = VBA.Split(SearchString, "*")
Set objFolder = New clsFSOFolder

With objFolder.Init(LookInFolder).Files

Select Case ExclusionSearch

Case False
For lngItem = 1 To .Count
Set objFile = .Item(lngItem)
If FitsPattern(strSearch(), objFile.name) Then
col.Add objFile, objFile.name
End If
Next lngItem

Case True
For lngItem = 1 To .Count
Set objFile = .Item(lngItem)
If Not FitsPattern(strSearch(), objFile.name) Then
col.Add objFile, objFile.name
End If
Next lngItem
End Select

End With

Set Search = Me
Set objFile = Nothing
Set objFolder = Nothing

End Function
' *****************************************************************************
Private Sub AddWithSearch(SearchString As String, File As clsFSOFile, _
FSOFiles As Object, FSOFile As Object)

Dim Search() As String

Search() = VBA.Split(SearchString, "*")

For Each FSOFile In FSOFiles
Set File = New clsFSOFile
If FitsPattern(Search(), File.Init(FSOFile.Path).name) Then
col.Add FSOFile
End If
Next FSOFile

End Sub

' *****************************************************************************
Private Function FitsPattern(SearchArray() As String, SearchedString As String) As Boolean
On Error GoTo err_handle

Dim lngSearchLen As Long ' length of the search string
Dim lngItemLen As Long ' length of the item in the array
Dim lngPos As Long ' search starting position
Dim lngLocation As Long ' location of the search string
Dim lngItem As Long ' array item
Dim UpperBound As Long, LowerBound As Long ' upper/lower bound of array
Dim blnSuccess As Boolean ' indicates success

lngSearchLen = VBA.Len(SearchedString) ' length of search string
lngPos = lngSearchLen ' initialize search position
UpperBound = UBound(SearchArray) ' upper bound of array
LowerBound = LBound(SearchArray) ' lower bound of array
blnSuccess = True ' initialize success to true

For lngItem = LowerBound To UpperBound
lngItemLen = VBA.Len(SearchArray(lngItem))
If lngItemLen Then

Select Case lngItem

Case (LowerBound + 1) To (UpperBound - 1)
' find the location of the search string from one of the patterns, starting from the last
' pattern search position
lngLocation = VBA.InStr(lngSearchLen - lngPos + 1, SearchedString, SearchArray(lngItem))
Select Case lngLocation

' if found, adjust the position forward
Case Is <> 0
lngPos = lngPos - lngLocation

Case Else ' not found or too late in the string
Err.Raise VBA.vbObjectError

End Select

Case LowerBound
' if lower bound of the array, then must start with the string
If VBA.Left$(SearchedString, lngItemLen) <> SearchArray(lngItem) Then _
Err.Raise VBA.vbObjectError

Case UpperBound
' position of previous search cannot be part of the last search
If (lngSearchLen - lngLocation) <>
Err.Raise VBA.vbObjectError
' if upper bound of the array, then must end with the search string
ElseIf VBA.Right$(SearchedString, lngItemLen) <> SearchArray(lngItem) Then
Err.Raise VBA.vbObjectError
End If

End Select

End If
Next lngItem

exit_err:
FitsPattern = blnSuccess

Exit Function
err_handle:

blnSuccess = False
Resume exit_err

End Function

clsFSOFolders - VBA

Option Explicit

' a late binding wrapper class for FSO Folders collection object
'


Private objFolders As Object
Private strFolders() As String

Public Event FolderIndexed(FSOFolder As clsFSOFolder)

' *****************************************************************************
Private Sub Class_Terminate()
Set objFolders = Nothing
End Sub

' *****************************************************************************
Public Function Init(Folders As Object) As clsFSOFolders
Set objFolders = Folders
Set Init = Me
ReDim strFolders(0)
End Function

' *****************************************************************************
Public Function Count() As Long
Count = objFolders.Count
End Function

' *****************************************************************************
Public Function Item(Index As Long) As clsFSOFolder

Dim objFold As clsFSOFolder

Set objFold = New clsFSOFolder
Set Item = objFold.Init(strFolders(Index))
Set objFold = Nothing

End Function

' *****************************************************************************
Public Function Index() As clsFSOFolders

Dim objFolder As Object
Dim lngItem As Long

ReDim strFiles(1 To objFolders.Count)

lngItem = 1
For Each objFolder In objFolders
strFiles(lngItem) = objFolder.Path
RaiseEvent FolderIndexed(Me.Item(lngItem))
lngItem = lngItem + 1
Next objFolder

Set objFolder = Nothing
Set Index = Me

End Function

clsFSOFolder - VBA

Option Explicit

' a late binding wrapper class for FSO Folder object
'


Private objFold As Object
Private WithEvents objFiles As clsFSOFiles
Private WithEvents objFolders As clsFSOFolders

Private lngSize As Currency
Private lngFileCount As Long

Public Event FileIndexed(FSOFile As clsFSOFile)
Public Event FolderIndexed(FSOFolder As clsFSOFolder)

' *****************************************************************************
Private Sub Class_Terminate()

Set objFolders = Nothing
Set objFiles = Nothing
Set objFold = Nothing

End Sub

' *****************************************************************************
Public Function Init(FolderPath As String) As clsFSOFolder
On Error GoTo err_handle

Dim objFSO As Object

Set objFSO = VBA.CreateObject("Scripting.FileSystemObject")
Set objFold = objFSO.GetFolder(VBA.Trim$(FolderPath))
Set objFSO = Nothing
Me.Changed

exit_err:
Set Init = Me

Exit Function

err_handle:
Set objFold = Nothing
Resume exit_err

End Function

' *****************************************************************************
Public Function Create(FolderPath As String, _
Optional RecursiveAttempts As Integer = 100) As clsFSOFolder
' creates the file if it doesn't already exist

Dim strParent As String

Select Case Me.Init(FolderPath).Exists Or (RecursiveAttempts = 0)

' path doesn't exist - attempt to create the parent directory
Case False
' strip off final slash
If VBA.Right$(FolderPath, 1) = "\" Then FolderPath = VBA.Left$(FolderPath, VBA.Len(FolderPath) - 1)
' get the parent directory
strParent = VBA.Left$(FolderPath, VBA.InStrRev(FolderPath, "\"))

' does the parent directory exist?
Select Case Me.Init(strParent).Exists

' create a new FSO and create the directory
Case True
Set Create = FSOCreate(FolderPath)

' recursively call create to create the parent
Case False
Me.Create strParent, RecursiveAttempts - 1
Set Create = FSOCreate(FolderPath)

End Select

' path exists - return reference to FSO Object
Case True
Set Create = Me

End Select

End Function
' *****************************************************************************
Private Function FSOCreate(FolderPath As String) As clsFSOFolder
On Error Resume Next

Dim objFSO As Object

Set objFSO = VBA.CreateObject("Scripting.FileSystemObject")
objFSO.CreateFolder FolderPath
Set objFSO = Nothing
Set FSOCreate = Me.Init(FolderPath)

End Function

' *****************************************************************************
Public Function DateCreated() As Date
DateCreated = objFold.DateCreated
End Function

' *****************************************************************************
Public Function Path() As String

Const acSLASH As String = "\"

Dim strReturn As String

strReturn = objFold.Path
Select Case VBA.Right$(strReturn, 1)
Case Is <> acSLASH
Path = strReturn & acSLASH
Case Else
Path = strReturn
End Select

End Function

' *****************************************************************************
Public Function DateLastAccessed() As Date
DateLastAccessed = objFold.DateLastAccessed
End Function

' *****************************************************************************
Public Function DateLastModified() As Date
DateLastModified = objFold.DateLastModified
End Function

' *****************************************************************************
Public Function name() As String
name = objFold.name
End Function

' *****************************************************************************
Public Function ParentFolder() As clsFSOFolder

Dim objFolder As clsFSOFolder
Set objFolder = New clsFSOFolder
Set ParentFolder = objFolder.Init(objFold.ParentFolder)
Set objFolder = Nothing

End Function

' *****************************************************************************
Public Function Size() As Currency
Size = objFold.Size
End Function

' *****************************************************************************
Public Function FolderType() As String
FolderType = objFold.FolderType
End Function

' *****************************************************************************
Public Function SubFolders() As clsFSOFolders

Select Case objFolders Is Nothing

Case False
Set SubFolders = objFolders

Case True
Set objFolders = New clsFSOFolders
Set SubFolders = objFolders.Init(objFold.SubFolders)

End Select

End Function

' *****************************************************************************
Public Function Files() As clsFSOFiles

Select Case objFiles Is Nothing

Case False
Set Files = objFiles

Case True
Set objFiles = New clsFSOFiles
Set Files = objFiles.Init(objFold.Files)

End Select

End Function

' *****************************************************************************
Public Function Move(Destination As String) As Boolean
On Error GoTo err_handle

objFold.Move Destination
Move = True

exit_err:

Exit Function

err_handle:
Move = False
Resume exit_err

End Function

' *****************************************************************************
Public Function Changed() As Boolean
' indicates if the contents of the folder have changed

Changed = (objFold.Files.Count <> lngFileCount) Or _
(Me.Size <> lngSize)

lngSize = Me.Size
lngFileCount = objFold.Files.Count

End Function

' *****************************************************************************
Public Function Exists() As Boolean

Exists = Not objFold Is Nothing

End Function

' *****************************************************************************
Private Sub objFiles_FileIndexed(FSOFile As clsFSOFile)

RaiseEvent FileIndexed(FSOFile)

End Sub

' *****************************************************************************
Private Sub objFolders_FolderIndexed(FSOFolder As clsFSOFolder)

RaiseEvent FolderIndexed(FSOFolder)

End Sub

clsFSOFiles - VBA

Option Explicit

' a late binding wrapper class for FSO Files Collection
'


Private blnIndex As Boolean

Private objFSO As Object
Private col As VBA.Collection

Public Event FileIndexed(FSOFile As clsFSOFile)

' *****************************************************************************
Private Sub Class_Terminate()
Set col = Nothing
Set objFSO = Nothing
End Sub

' *****************************************************************************
Public Function Init(Files As Object) As clsFSOFiles
Set objFSO = Files
Set Init = Me
blnIndex = True
End Function

' *****************************************************************************
Public Function Count() As Long

If blnIndex Then Me.Index
Count = col.Count

End Function

' *****************************************************************************
Public Function Item(Index As Long) As clsFSOFile

If blnIndex Then Me.Index
Set Item = col.Item(Index)

End Function

' *****************************************************************************
Public Function Index() As clsFSOFiles

Dim objFile As Object
Dim objFSOFile As clsFSOFile

blnIndex = False
Set col = New VBA.Collection

For Each objFile In objFSO
Set objFSOFile = New clsFSOFile
col.Add objFSOFile.Init(objFile.Path)
RaiseEvent FileIndexed(objFSOFile)
Next objFile

Set objFile = Nothing
Set objFSOFile = Nothing

Set Index = Me

End Function

clsFSOFile - VBA

Option Explicit

' a late binding wrapper class for FSO Files
'

Private objFile As Object

' *****************************************************************************
Private Sub Class_Terminate()
Set objFile = Nothing
End Sub

' *****************************************************************************
Public Function Init(FilePath As String) As clsFSOFile
On Error GoTo err_handle

Dim objFSO As Object

Set objFSO = VBA.CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.GetFile(FilePath)
Set objFSO = Nothing

exit_err:
Set Init = Me

Exit Function
err_handle:
Set objFile = Nothing
Resume exit_err

End Function
' *****************************************************************************
Public Function DateLastModified() As Date
DateLastModified = objFile.DateLastModified
End Function

' *****************************************************************************
Public Function DateCreated() As Date
DateCreated = objFile.DateCreated
End Function

' *****************************************************************************
Public Function Path() As String
Path = objFile.Path
End Function

' *****************************************************************************
Public Function DateLastAccessed() As Date
DateLastAccessed = objFile.DateLastAccessed
End Function

' *****************************************************************************
Public Function name() As String
name = objFile.name
End Function

' *****************************************************************************
Public Function ParentFolder() As clsFSOFolder

Dim objFolder As clsFSOFolder
Set objFolder = New clsFSOFolder
Set ParentFolder = objFolder.Init(objFile.ParentFolder)
Set objFolder = Nothing

End Function

' *****************************************************************************
Public Function Size() As Long
Size = objFile.Size
End Function

' *****************************************************************************
Public Function FileType() As String
FileType = objFile.Type
End Function

' *****************************************************************************
Public Function Move(Destination As String, _
Optional Overwrite As Boolean = True) As Boolean
On Error GoTo err_handle

If VBA.Right$(Destination, 1) <> "\" Then Destination = Destination & "\"

objFile.Move Destination & Me.name
Move = True
exit_err:

Exit Function

err_handle:
Select Case Err.Number
Case 58 ' enmError.errFileExists
If Overwrite Then
objFile.Copy Destination, Overwrite
objFile.Delete
End If
Case Else
Move = False
End Select

Resume exit_err
End Function

' *****************************************************************************
Public Function Extension() As String

Dim strName As String
Dim lngPos As Long

strName = Me.name
lngPos = VBA.InStrRev(strName, ".")
Select Case lngPos
Case Is > 0
Extension = VBA.LCase$(VBA.Mid$(strName, lngPos + 1, VBA.Len(strName)))
Case Else
Extension = VBA.vbNullString
End Select

End Function

' *****************************************************************************
Public Function Exists() As Boolean
Exists = Not (objFile Is Nothing)
End Function

' *****************************************************************************
Public Function Delete() As Boolean
' returns true if successful, false if not. this method will not return
' any errors if the file does not exist
On Error Resume Next

objFile.Delete
Delete = Not VBA.CBool(Err.Number)
Err.Clear
Set objFile = Nothing

End Function