Tom’s Blog

January 2, 2010

Using Extension Methods and the Win32 API to Efficiently Enumerate the File System

Filed under: .NET,C#,P/Invoke,Uncategorized — Tom Shelton @ 11:52 am

I’ve seen the question come up a couple of times lately on various forums, asking how to get only the first file from a directory listing in a directory with a large number of files.   This is problematic, because as of .NET 3.5 the System.IO methods that are responsible for enumerating the file system (System.IO.Directory.GetFiles for example) do a complete enumeration before returning.  That means if you just want the first 5 files, GetFiles will still enumerate the entire directory before returning.  Further, wouldn’t it be nice to have methods where you could use LINQ queries on file attributes, etc?  If your paying attention to .NET 4.0, you will notice that there are methods now of the Directory/DirectoryInfo classes that will provide just these options – System.Directory.EnumerateFiles, etc.  These methods provide IEnumerable<> return values rather then the fixed array.  Basically, they enumerate and return each value one at a time.

The good news is that with C# 3.0 – we don’t actually have to wait until .NET 4.0 to have these methods now.  By using a little P/Invoke magic and C# 3.0’s extension method capability, we can write code like this today:

   1: class Program
   2: {
   3:     static void Main ( string[] args )
   4:     {
   5:         string path = Environment.GetFolderPath ( Environment.SpecialFolder.MyDocuments );
   6:         DirectoryInfo root = new DirectoryInfo ( path );
   7:
   8:         Console.WriteLine ( root.EnumerateFiles ().First ().Name );
   9:     }
  10: }

Or even better, we can do stuff like this:

   1: class Program
   2: {
   3:    static void Main ( string[] args )
   4:    {
   5:        string path = Environment.GetFolderPath ( Environment.SpecialFolder.MyDocuments );
   6:        DirectoryInfo root = new DirectoryInfo ( path );
   7:
   8:        // get all doc files created before Nov. 1st 2009
   9:        var fileQuery = from fileInfo in root.EnumerateFiles ( "*.doc" )
  10:                        where fileInfo.CreationTime.Date.CompareTo ( new DateTime ( 2009, 11, 1 ) ) == -1
  11:                        select fileInfo;
  12:
  13:        foreach ( var fileInfo in fileQuery )
  14:            Console.WriteLine ( fileInfo.Name );
  15:    }
  16: }

To handle the actual enumeration of the files we will use the Windows API.  The functions we are interested in are FindFirstFile, FindNextFile, and FindClose.  Here are the Native Win32 calls and classes used by the demo extension methods above:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.IO;
   5: using System.Runtime.InteropServices;
   6: using Microsoft.Win32.SafeHandles;
   7:
   8: namespace FireAnt.IO
   9: {
  10:     internal static class NativeWin32
  11:     {
  12:         public const int MAX_PATH = 260;
  13:
  14:         /// <summary>
  15:         /// Win32 FILETIME structure.  The win32 documentation says this:
  16:         /// "Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)."
  17:         /// </summary>
  18:         /// <see cref="http://msdn.microsoft.com/en-us/library/ms724284%28VS.85%29.aspx"/>
  19:         [StructLayout ( LayoutKind.Sequential )]
  20:         public struct FILETIME
  21:         {
  22:             public uint dwLowDateTime;
  23:             public uint dwHighDateTime;
  24:         }
  25:
  26:         /// <summary>
  27:         /// The Win32 find data structure.  The documentation says:
  28:         /// "Contains information about the file that is found by the FindFirstFile, FindFirstFileEx, or FindNextFile function."
  29:         /// </summary>
  30:         /// <see cref="http://msdn.microsoft.com/en-us/library/aa365740%28VS.85%29.aspx"/>
  31:         [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
  32:         public struct WIN32_FIND_DATA
  33:         {
  34:             public FileAttributes dwFileAttributes;
  35:             public FILETIME ftCreationTime;
  36:             public FILETIME ftLastAccessTime;
  37:             public FILETIME ftLastWriteTime;
  38:             public uint nFileSizeHigh;
  39:             public uint nFileSizeLow;
  40:             public uint dwReserved0;
  41:             public uint dwReserved1;
  42:
  43:             [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_PATH)]
  44:             public string cFileName;
  45:
  46:             [MarshalAs ( UnmanagedType.ByValTStr, SizeConst=14)]
  47:             public string cAlternateFileName;
  48:         }
  49:
  50:         /// <summary>
  51:         /// Searches a directory for a file or subdirectory with a name that matches a specific name (or partial name if wildcards are used).
  52:         /// </summary>
  53:         /// <param name="lpFileName">The directory or path, and the file name, which can include wildcard characters, for example, an asterisk (*) or a question mark (?). </param>
  54:         /// <param name="lpFindData">A pointer to the WIN32_FIND_DATA structure that receives information about a found file or directory.</param>
  55:         /// <returns>
  56:         /// If the function succeeds, the return value is a search handle used in a subsequent call to FindNextFile or FindClose, and the lpFindFileData parameter contains information about the first file or directory found.
  57:         /// If the function fails or fails to locate files from the search string in the lpFileName parameter, the return value is INVALID_HANDLE_VALUE and the contents of lpFindFileData are indeterminate.
  58:         ///</returns>
  59:         ///<see cref="http://msdn.microsoft.com/en-us/library/aa364418%28VS.85%29.aspx"/>
  60:         [DllImport("kernel32", CharSet=CharSet.Auto, SetLastError=true)]
  61:         public static extern SafeSearchHandle FindFirstFile ( string lpFileName, out WIN32_FIND_DATA lpFindData );
  62:
  63:         /// <summary>
  64:         /// Continues a file search from a previous call to the FindFirstFile or FindFirstFileEx function.
  65:         /// </summary>
  66:         /// <param name="hFindFile">The search handle returned by a previous call to the FindFirstFile or FindFirstFileEx function.</param>
  67:         /// <param name="lpFindData">A pointer to the WIN32_FIND_DATA structure that receives information about the found file or subdirectory.
  68:         /// The structure can be used in subsequent calls to FindNextFile to indicate from which file to continue the search.
  69:         /// </param>
  70:         /// <returns>
  71:         /// If the function succeeds, the return value is nonzero and the lpFindFileData parameter contains information about the next file or directory found.
  72:         /// If the function fails, the return value is zero and the contents of lpFindFileData are indeterminate.
  73:         /// </returns>
  74:         /// <see cref="http://msdn.microsoft.com/en-us/library/aa364428%28VS.85%29.aspx"/>
  75:         [DllImport("kernel32", CharSet=CharSet.Auto, SetLastError=true)]
  76:         public static extern bool FindNextFile ( SafeSearchHandle hFindFile, out WIN32_FIND_DATA lpFindData );
  77:
  78:         /// <summary>
  79:         /// Closes a file search handle opened by the FindFirstFile, FindFirstFileEx, or FindFirstStreamW function.
  80:         /// </summary>
  81:         /// <param name="hFindFile">The file search handle.</param>
  82:         /// <returns>
  83:         /// If the function succeeds, the return value is nonzero.
  84:         /// If the function fails, the return value is zero. 
  85:         /// </returns>
  86:         /// <see cref="http://msdn.microsoft.com/en-us/library/aa364413%28VS.85%29.aspx"/>
  87:         [DllImport("kernel32", SetLastError=true)]
  88:         public static extern bool FindClose ( IntPtr hFindFile );
  89:
  90:         /// <summary>
  91:         /// Class to encapsulate a seach handle returned from FindFirstFile.  Using a wrapper
  92:         /// like this ensures that the handle is properly cleaned up with FindClose.
  93:         /// </summary>
  94:         public class SafeSearchHandle : SafeHandleZeroOrMinusOneIsInvalid
  95:         {
  96:             public SafeSearchHandle () : base ( true ) { }
  97:
  98:             protected override bool ReleaseHandle ()
  99:             {
 100:                 return NativeWin32.FindClose ( base.handle );
 101:             }
 102:         }
 103:     }
 104: }

And here are the actual extension methods:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.IO;
   5:
   6: namespace FireAnt.IO
   7: {
   8:     /// <summary>
   9:     /// Static class to contain extension methods
  10:     /// </summary>
  11:     public static class FileSystemExtensions
  12:     {
  13:
  14:         public static IEnumerable<DirectoryInfo> EnumerateDirectories ( this DirectoryInfo target )
  15:         {
  16:             return EnumerateDirectories ( target, "*" );
  17:         }
  18:
  19:         public static IEnumerable<DirectoryInfo> EnumerateDirectories ( this DirectoryInfo target, string searchPattern )
  20:         {
  21:             string searchPath = Path.Combine ( target.FullName, searchPattern );
  22:             NativeWin32.WIN32_FIND_DATA findData;
  23:             using (NativeWin32.SafeSearchHandle hFindFile = NativeWin32.FindFirstFile ( searchPath, out findData ))
  24:             {
  25:                 if ( !hFindFile.IsInvalid )
  26:                 {
  27:                     do
  28:                     {
  29:                         if ( ( findData.dwFileAttributes & FileAttributes.Directory ) != 0 && findData.cFileName != "." && findData.cFileName != ".." )
  30:                         {
  31:                             yield return new DirectoryInfo ( Path.Combine ( target.FullName, findData.cFileName ) );
  32:                         }
  33:                     } while ( NativeWin32.FindNextFile ( hFindFile, out findData ) );
  34:                 }
  35:             }
  36:
  37:         }
  38:
  39:         public static IEnumerable<FileInfo> EnumerateFiles ( this DirectoryInfo target )
  40:         {
  41:            return EnumerateFiles ( target, "*" );
  42:         }
  43:
  44:         public static IEnumerable<FileInfo> EnumerateFiles ( this DirectoryInfo target, string searchPattern )
  45:         {
  46:             string searchPath = Path.Combine ( target.FullName, searchPattern );
  47:             NativeWin32.WIN32_FIND_DATA findData;
  48:             using ( NativeWin32.SafeSearchHandle hFindFile = NativeWin32.FindFirstFile ( searchPath, out findData ) )
  49:             {
  50:                 if ( !hFindFile.IsInvalid )
  51:                 {
  52:                     do
  53:                     {
  54:                         if ( ( findData.dwFileAttributes & FileAttributes.Directory ) == 0 && findData.cFileName != "." && findData.cFileName != ".." )
  55:                         {
  56:                             yield return new FileInfo ( Path.Combine ( target.FullName, findData.cFileName ) );
  57:                         }
  58:                     } while ( NativeWin32.FindNextFile ( hFindFile, out findData ) );
  59:                 }
  60:             }
  61:
  62:         }
  63:     }
  64: }

With a bit more work, the full range of file search operations could be added to these extension methods – for instance supporting the SearchOptions overloads.  I hope this demo illustrates the power of Extension methods and C# iterators ( a feature introduced in C# 2.0).

Article Source Code.

October 7, 2009

Creating a Simple Hotkey Component using RegisterHotKey

Filed under: .NET,C#,P/Invoke,Visual Basic — Tom Shelton @ 12:47 pm

A while ago, I needed to create a simple tray application that had to be able be brought into focus using a global hotkey.  I did a bit of research and came across the RegisterHotKey API function.  On my first attempt at using it, I created a simple class that I used to subclass any window handle that was passed into the constructor.  It  worked well – until you wanted to have more then one hotkey and it seemed to me it might be more user friendly as a component then as a class.  So, I rewrote the code as a windows forms component – and in the process I even created a custom type editor for the HotKey.Modifiers property.

Anyway, I’m posting two projects – the first is the HotKeyLib project that contains the Hotkey component.  The second is a project (both C# and VB.NET) that make use of the component.  You will need to compile the HotKeyLib project and then add it to your IDE’s toolbox to compile the second project.  The reason I did it this way was to show that the custom icon of the hotkey component will indeed show up in the IDE toolbox.

Anyway, hope this code proves helpful.

August 17, 2009

ByVal vs ByRef – What is the Difference?

Filed under: General,Visual Basic — Tom Shelton @ 10:44 pm

Over the years, I’ve noticed that there seems to be a lot of confusion about the difference between passing a parameter ByVal vs. ByRef.    So, I thought I’d spend some time writing something on this topic.  It should be noted, that while I will be discussing this from the view point of VB.NET, the same basic concepts also apply to VB.CLASSIC – it’s just that some of the terminology is different.

First, let’s cover the definition of these two keywords as they are presented in the Visual Basic documentation.

ByVal

Specifies that an argument is passed in such a way that the called procedure or property cannot change the value of a variable underlying the argument in the calling code.

ByRef

Specifies that an argument is passed in such a way that the called procedure can change the value of a variable underlying the argument in the calling code.

On the surface, these two statements seem simple enough.  ByVal (short for By Value) means that the called procedure can not alter the value that was passed.  In essence, when you pass ByVal – a copy of the original value is made and passed to the procedure.  ByRef (short for By Reference) on the other hand is just the opposite.  When, a value is passed ByRef, then a reference – aka pointer (in fact it is more accurate to say that it is a C++ reference then a pointer) – to the original value is passed, so any changes made to this value alter the callers state.  Just to test this out, lets create a little test code and see what happens:

 

   1: Option Explicit On
   2: Option Strict On
   3:  
   4: Module Module1
   5:  
   6:     Sub Main()
   7:         Dim i As Integer = 0
   8:  
   9:         ' Pass an integer ByVal
  10:         Console.WriteLine("Before ByVal Call: i = {0}", i)
  11:         PassIntByVal(i)
  12:         Console.WriteLine("After ByVal Call: i = {0}", i)
  13:  
  14:         Console.WriteLine()
  15:  
  16:         ' Pass an integer ByRef
  17:         Console.WriteLine("Before ByRef Call: i = {0}", i)
  18:         PassIntByRef(i)
  19:         Console.WriteLine("After ByRef Call: i = {0}", i)
  20:  
  21:     End Sub
  22:  
  23:     Sub PassIntByVal(ByVal i As Integer)
  24:         i = 5
  25:     End Sub
  26:  
  27:     Sub PassIntByRef(ByRef i As Integer)
  28:         i = 5
  29:     End Sub
  30:  
  31: End Module

And our output:

output001

So far so good.  It looks exactly like we should expect given the definitions of ByVal and ByRef.  So, if things work according to the definition, why the confusion?   Well, lets expand this simple example a bit and see if things continue to hold up – this time by creating a structure and putting it to the same test as above:

   1: Option Explicit On
   2: Option Strict On
   3:  
   4: Module Module1
   5:  
   6:     Sub Main()
   7:         Dim i As Integer = 0
   8:  
   9:         ' Pass an integer ByVal
  10:         Console.WriteLine("Before ByVal Call: i = {0}", i)
  11:         PassIntByVal(i)
  12:         Console.WriteLine("After ByVal Call: i = {0}", i)
  13:  
  14:         Console.WriteLine()
  15:  
  16:         ' Pass an integer ByRef
  17:         Console.WriteLine("Before ByRef Call: i = {0}", i)
  18:         PassIntByRef(i)
  19:         Console.WriteLine("After ByRef Call: i = {0}", i)
  20:  
  21:         Console.WriteLine()
  22:  
  23:         Dim ts As TestStructure
  24:  
  25:         ' Pass a structure ByVal
  26:         Console.WriteLine("Before ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  27:         PassStructureByVal(ts)
  28:         Console.WriteLine("After ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  29:  
  30:         Console.WriteLine()
  31:  
  32:         ' Pass a strucutre ByRef
  33:         Console.WriteLine("Before ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  34:         PassStructureByRef(ts)
  35:         Console.WriteLine("After ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  36:  
  37:     End Sub
  38:  
  39:     Sub PassIntByVal(ByVal i As Integer)
  40:         i = 5
  41:     End Sub
  42:  
  43:     Sub PassIntByRef(ByRef i As Integer)
  44:         i = 5
  45:     End Sub
  46:  
  47:     Sub PassStructureByVal(ByVal s As TestStructure)
  48:         s.i = 5
  49:         s.j = 10
  50:     End Sub
  51:  
  52:     Sub PassStructureByRef(ByRef s As TestStructure)
  53:         s.i = 5
  54:         s.j = 10
  55:     End Sub
  56:  
  57:     Structure TestStructure
  58:         Public i As Integer
  59:         Public j As Integer
  60:     End Structure
  61: End Module

And our output:

output002

The output should be consistent with our expectations.  Again, what’s the problem?  I’m going to do one more expansion of our code – lets add a custom class into the mix:

   1: Option Explicit On
   2: Option Strict On
   3:  
   4: Module Module1
   5:  
   6:     Sub Main()
   7:         Dim i As Integer = 0
   8:  
   9:         ' Pass an integer ByVal
  10:         Console.WriteLine("Before ByVal Call: i = {0}", i)
  11:         PassIntByVal(i)
  12:         Console.WriteLine("After ByVal Call: i = {0}", i)
  13:  
  14:         Console.WriteLine()
  15:  
  16:         ' Pass an integer ByRef
  17:         Console.WriteLine("Before ByRef Call: i = {0}", i)
  18:         PassIntByRef(i)
  19:         Console.WriteLine("After ByRef Call: i = {0}", i)
  20:  
  21:         Console.WriteLine()
  22:  
  23:         Dim ts As TestStructure
  24:  
  25:         ' Pass a structure ByVal
  26:         Console.WriteLine("Before ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  27:         PassStructureByVal(ts)
  28:         Console.WriteLine("After ByVal Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  29:  
  30:         Console.WriteLine()
  31:  
  32:         ' Pass a strucutre ByRef
  33:         Console.WriteLine("Before ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  34:         PassStructureByRef(ts)
  35:         Console.WriteLine("After ByRef Call: ts.i = {0}, ts.j={1}", ts.i, ts.j)
  36:  
  37:         Console.WriteLine()
  38:  
  39:         Dim tc As New TestClass
  40:  
  41:         ' Pass a class ByVal
  42:         Console.WriteLine("Before ByVal Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
  43:         PassClassByVal(tc)
  44:         Console.WriteLine("After ByVal Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
  45:  
  46:         Console.WriteLine()
  47:  
  48:         ' Pass a class ByRef
  49:         Console.WriteLine("Before ByRef Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
  50:         PassClassByRef(tc)
  51:         Console.WriteLine("After ByRef Call: tc.i = {0}, tc.j={1}", tc.i, tc.j)
  52:  
  53:     End Sub
  54:  
  55:     Sub PassIntByVal(ByVal i As Integer)
  56:         i = 5
  57:     End Sub
  58:  
  59:     Sub PassIntByRef(ByRef i As Integer)
  60:         i = 5
  61:     End Sub
  62:  
  63:     Sub PassStructureByVal(ByVal s As TestStructure)
  64:         s.i = 5
  65:         s.j = 10
  66:     End Sub
  67:  
  68:     Sub PassStructureByRef(ByRef s As TestStructure)
  69:         s.i = 5
  70:         s.j = 10
  71:     End Sub
  72:  
  73:     Sub PassClassByVal(ByVal c As TestClass)
  74:         c.i = 5
  75:         c.j = 10
  76:     End Sub
  77:  
  78:     Sub PassClassByRef(ByRef c As TestClass)
  79:         c.i = 6
  80:         c.j = 11
  81:     End Sub
  82:  
  83:     Structure TestStructure
  84:         Public i As Integer
  85:         Public j As Integer
  86:     End Structure
  87:  
  88:     Class TestClass
  89:         Public i As Integer
  90:         Public j As Integer
  91:     End Class
  92:  
  93: End Module

And the output:

output003

Take a look at the lines of output for the tc variable.  Have we been mislead by the documentation?   Because, on the surface at least, it sure appears as if tc was modified when passed ByVal.   The answer to this question is no – ByVal still worked exactly as it was supposed to. 

To understand why these results are in fact correct, requires a bit of understanding of the .NET type system.  The .NET CTS – or Common Type System – defines two basic type categories (each of which is further subdivided).  These categories are Value types and Reference types.  Here is what the documentation has to say regarding these two categories:

    • Value types

      Value types directly contain their data, and instances of value types are either allocated on the stack or allocated inline in a structure. Value types can be built-in (implemented by the runtime), user-defined, or enumerations. For a list of built-in value types, see the .NET Framework Class Library.

    • Reference types

      Reference types store a reference to the value’s memory address, and are allocated on the heap. Reference types can be self-describing types, pointer types, or interface types. The type of a reference type can be determined from values of self-describing types. Self-describing types are further split into arrays and class types. The class types are user-defined classes, boxed value types, and delegates.

Summary:  A value type variable directly contains it’s data, a reference type variable does not.   In practical terms, this means that the behavior demonstrated by the example code is in fact completely consistent with the documented behavior of ByVal and ByRef.  When passing ByVal, the copy that is made is the contents of the variable – in the case of a reference type, the value that the variable contains is not the instance, but the memory address of the instance on the heap.  Maybe a small diagram will help clarify:

heap

What this means in practical terms is that, if you pass an object ByVal – the objects properties can be modified, but the reference can not:

   1: Option Explicit On
   2: Option Strict On
   3:  
   4: Module Module1
   5:  
   6:     Sub Main()
   7:         Dim tc As TestClass = Nothing
   8:  
   9:         ' pass by value
  10:         Console.WriteLine("Passing tc ByVal")
  11:         CreateTCByVal(tc)
  12:         Console.WriteLine(If(tc IsNot Nothing, String.Format("tc.i = {0}, tc.j={1}", tc.i, tc.j), "tc is Nothing"))
  13:  
  14:         Console.WriteLine()
  15:  
  16:         ' pass by reference
  17:         Console.WriteLine("Passing tc ByRef")
  18:         CreateTCByRef(tc)
  19:         Console.WriteLine(If(tc IsNot Nothing, String.Format("tc.i = {0}, tc.j={1}", tc.i, tc.j), "tc is Nothing"))
  20:     End Sub
  21:  
  22:     Sub CreateTCByVal(ByVal c As TestClass)
  23:         c = New TestClass()
  24:         c.i = 5
  25:         c.j = 6
  26:     End Sub
  27:  
  28:     Sub CreateTCByRef(ByRef c As TestClass)
  29:         c = New TestClass()
  30:         c.i = 5
  31:         c.j = 6
  32:     End Sub
  33:  
  34:     Class TestClass
  35:         Public i As Integer
  36:         Public j As Integer
  37:     End Class
  38:  
  39: End Module

And the output:

output004

I hope this brief explanation of ByVal and ByRef can help clear up some of the confusion that seems to exist regarding these to argument passing methods.

May 11, 2009

A P/Invoke Primer for the Experienced VB6 Programmer

Filed under: .NET,P/Invoke,Visual Basic — Tom Shelton @ 10:52 pm

"In VB6 I used to use SomeSuchAPI to perform task X – I have tried to use SomeSuchAPI in my new SuperVelocityStrumCruncher app written in VB.NET, but it won’t work!  Help!"  And on goes the refrain. 

When I see these types of questions, nine times out of ten, I know the answer before I even read the question.  Or, that is to say, I can pretty much surmise that the answer will fall into one of a couple of typical traps that the experienced VB6 user faces when deciding to use P/Invoke in VB.NET (which, is a pretty relevant question because many times the task you were using the API for in VB6, may just be taken care of by the .NET framework).

So, I’m going to cover a few tips that should help the experienced VB6 user successfully make use of Windows or other API calls from VB.NET.


Know Your Data Types

That’s right – know your data types.  For better or for worse, many of the integral data types that you used to know and love in VB6 have changed sizes in VB.NET.  For example, Long is no longer a 32-bit Integer – it’s 64-bit.  So, forget what you knew about integer types and their sizes in VB6 and learn them a new in VB.NET.

Here is a quick conversion table that might help speed along this task:

Size/Sign VB6 Data Type VB.NET Data Type
8 bits/unsigned Byte Byte
8 bits/signed N/A SByte
16 bits/unsigned N/A UShort
16 bits/signed Integer Short
32 bits/unsigned N/A UInteger
32 bits/signed Long Integer
64 bits/unsigned N/A ULong
64 bits/signed N/A Long
This table is based on the information found here.

So…  What does all of this mean in the actual context of a Windows API call?  Well, lets look at the GetWindowsDirectory API call.  A typical declaration for this in VB6 might be:

   1: Private Declare Function GetWindowsDirectory Lib "KERNEL32" Alias "GetWindowsDirectoryA" _
   2:     (ByVal lpBuffer As String, ByVal nSize As Long) As Long

This call is incorrect in VB.NET – we can see that we are passing a Long value to the nSize parameter, which is a 32-bit integer in VB6, but is 64-bit in VB.NET.  This can cause problems as it can unbalance the functions call stack.  To correct this we can declare the function in VB.NET like so:

   1: Private Declare Function GetWindowsDirectory Lib "KERNEL32" Alias "GetWindowsDirectoryA" _
   2:     (ByVal lpBuffer As String, ByVal nSize As Integer) As Integer

If you were to use this declaration in VB.NET, it would work – but, there are a couple of other levels of enlightenment that can be achieved here on our way to API Nirvana! 

Drop The Alias

Any one else remember the bad old days when a seemingly simple call to an API call from VB could cause gyrations such as these to occur on NT based OS’s?

funny_conversion_flow_chart

Good Times!  Good Times!

Well, fortunately, the VB.NET marshaller has improved significantly when it comes to handling of strings.  For instance, it can call the W functions directly without the need for hacks using StrPtr or creating custom API typelibs.  And, as if that wasn’t good news, it actually gets better!  You don’t even have to alias your functions anymore. 

The venerable old Declare statement has a new option in VB.NET that lets you declare the character set that should be used when calling the function.  There are 3 choices – Ansi, Unicode, and Auto.  When dealing with the A/W function set of the Windows API, Auto is often a good choice because you can just declare the function and let the .NET runtime worry about if it should call the A or the W version.

Here’s our function declaration again with a bit more .NET goodness:

   1: Private Declare Auto Function GetWindowsDirectory Lib "KERNEL32" _
   2:     (ByVal lpBuffer As String, ByVal nSize As Integer) As Integer

Isn’t that nice!

Mind Your Buffers

Ok, I want to point out one more minor adjustment that probably should be made to our Declare statement.  In the name of correctness, since the buffer we are passing in to this function is intended to be filled by the API call, we should not be passing the lpBuffer parameter as String.  See in a nod to compatibility with it’s predecessor, the VB.NET marshaller – unlike C#’s – does not respect the immutability of strings and will go through the extra gyrations necessary to make sure you get the result you have come to expect in your long association with VB.CLASSIC.  With out getting into arguments about computability vs correctness – I’m simply going to show you the recommended way of declaring string buffers that are intended to be filled by called API’s and you are free to make your own decision as to which way to go with this:

   1: Private Declare Auto Function GetWindowsDirectory Lib "KERNEL32" _
   2:     (ByVal lpBuffer As System.Text.StringBuilder, ByVal nSize As Integer) As Integer

Keep in mind, the use of StringBuilder is only recommended when the string is intended to be modified by the called function – if it’s a simple constant string buffer, then String is definitely the appropriate type.  Here is a complete sample using our declare statement:

   1: Option Strict On
   2: Option Explicit On
   3:  
   4: Imports System
   5: Imports System.Text
   6:  
   7: Module Module1
   8:     Private Const MAX_PATH As Integer = 260
   9:     Private Declare Auto Function GetWindowsDirectory Lib "KERNEL32" _
  10:         (ByVal lpBuffer As StringBuilder, ByVal nSize As Integer) As Integer
  11:  
  12:     Sub Main()
  13:         Console.WriteLine(GetWindowsDirectory())
  14:     End Sub
  15:  
  16:     Private Function GetWindowsDirectory() As String
  17:         Dim buffer As New StringBuilder(MAX_PATH)
  18:         GetWindowsDirectory(buffer, buffer.Capacity)
  19:         Return buffer.ToString()
  20:     End Function
  21: End Module

Handle Your Handles

The last bit of advice that I’m going to render in this article is about handles.  In the Windows API the concept of handles is very important, and handles are used all over the place.  When ever you are dealing with a handle type (such as HWND), then it is recommended that you use System.IntPtr as the type for that parameter over Integer.  The main reason for this is that the IntPtr is a wrapper over an unsigned system sized integer – so that means that it is 32-bit on 32-bit systems and 64-bit on 64-bit systems.  See how that works?  Anyway, it might make your porting life in the future easier if you remember that little tip.

May 5, 2009

Using C# and WMI to Create and Configure IIS6 Web Sites

Filed under: C# — Tom Shelton @ 10:53 pm

Recently, I was worked on a project that required me to automate the installation and creation of web sites on IIS6.  I didn’t want to do this using VBScript or another WSH language because this was going to be used as part of a larger application.  I made some exploratory attempts using the ADSI interfaces, but did not find them satisfactory due to performance concerns.  After a little research, I discovered that since I was dealing with IIS6, that there were WMI interfaces available – so I decided to give that a whirl.  What I discovered, was that the while the performance using WMI was not what I would call spectacular, it was definitely a large improvement over using the ADSI interfaces.  The classes and code I’m sharing here are basic implementations, that accomplish only the basic tasks that I needed – so, there is definitely room for expansion on the part of the reader.  This is only meant as a starting point – I am still actively extending and refactoring these implementations my self.

What I was creating was basically a generic deployment tool – basically, this tool takes a package (a zip file) that contains the web site content and directory structure and XML document that describes the details of the deployment based on the machine name in the infrastructure.  This contains things like the name of directory to extract the package to, any web.config changes that need to be applied for the environment, and most importantly to this article a description of the IIS web site that should be created.

The first class I will share is the base class for most of the classes in my hierarchy – WmiObjectBase.

 

   1: using System.Collections.Generic;
   2: using System.Management;
   3:  
   4: namespace FireAnt.IIS.Util
   5: {
   6:     /// <summary>
   7:     /// Base object for WMI objects
   8:     /// </summary>
   9:     public abstract class WmiObjectBase
  10:     {
  11:         /// <summary>
  12:         /// Create and connect a management scope for use by subclasses
  13:         /// </summary>
  14:         /// <param name="path">The management path</param>
  15:         protected WmiObjectBase(string path)
  16:         {
  17:             ConnectionOptions connectionOptions = new ConnectionOptions ();
  18:             connectionOptions.Authentication = AuthenticationLevel.PacketPrivacy;
  19:             this.Scope = new ManagementScope (path , connectionOptions );
  20:             this.Scope.Connect ();
  21:         }
  22:  
  23:         /// <summary>
  24:         /// Allow subclasses to pass in an existing scope
  25:         /// </summary>
  26:         /// <param name="scope">A management scope object</param>
  27:         protected WmiObjectBase ( ManagementScope scope )
  28:         {
  29:             this.Scope = scope;
  30:             if ( !this.Scope.IsConnected )
  31:                 this.Scope.Connect ();
  32:         }
  33:         
  34:         /// <summary>
  35:         /// Let subclasses get their scope object
  36:         /// </summary>
  37:         protected ManagementScope Scope { get; private set; }
  38:  
  39:         /// <summary>
  40:         /// Allow subobjects to peform queries
  41:         /// </summary>
  42:         /// <param name="query">The object query to perform</param>
  43:         /// <returns>A ManagementObjectCollection containing the results of the query</returns>
  44:         protected ManagementObjectCollection this[ObjectQuery query]
  45:         {
  46:             get
  47:             {
  48:                 return new ManagementObjectSearcher ( this.Scope, query ).Get ();
  49:             }
  50:         }
  51:  
  52:  
  53:         /// <summary>
  54:         /// Apply settings to a Management object.
  55:         /// </summary>
  56:         /// <param name="target">The ManagamentObject to apply the settings to</param>
  57:         /// <param name="properties">A Dictionary object containing the properties and their value.</param>
  58:         /// <remarks>
  59:         /// The properties Dictionary contains the name of the property as the key, and the value of the property as the value.
  60:         /// </remarks>
  61:         protected void ApplySettings ( ManagementObject target, Dictionary<string, object> properties )
  62:         {
  63:             foreach ( var kv in properties )
  64:             {
  65:                 target.Properties[kv.Key].Value = kv.Value;
  66:             }
  67:             target.Put ();
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Allow subclasses and easy method of creating new ManagementObject instances
  72:         /// </summary>
  73:         /// <param name="path">The management path of the object</param>
  74:         /// <returns>The instance of the object</returns>
  75:         protected ManagementObject CreateManagementObject ( string path )
  76:         {
  77:             ManagementClass template = new ManagementClass ( this.Scope, new ManagementPath ( path ), null );
  78:             return template.CreateInstance ();
  79:         }
  80:  
  81:         /// <summary>
  82:         /// Allow subclasses to get instances of existing objects
  83:         /// </summary>
  84:         /// <param name="path">the management path</param>
  85:         /// <returns>An instance of the object</returns>
  86:         protected ManagementObject GetInstance ( string path )
  87:         {
  88:             return new ManagementObject ( this.Scope, new ManagementPath ( path ), null );
  89:         }
  90:     }
  91: }

This class simply provides some common functionality for the various WMI based objects in my library.

The main class in the library, is the InternetInformationServer class.  This class holds and manages the connection to an IIS6 instance.  It allows you to create and manipulate the various server objects, such as web sites and application pools:

 

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Management;
   6: using System.Text.RegularExpressions;
   7:  
   8: namespace FireAnt.IIS.Util
   9: {
  10:     public class InternetInformationServer : WmiObjectBase
  11:     {
  12:         /// <summary>
  13:         /// Create an instance of the InternetInformationServer class that is connected to the
  14:         /// local IIS instance.
  15:         /// </summary>
  16:         public InternetInformationServer () : base ( @"\\.\root\MicrosoftIISV2" ) { }
  17:  
  18:         /// <summary>
  19:         /// Create an instance of the InternetInformationServer class that is connected to the
  20:         /// instance defined by target.
  21:         /// </summary>
  22:         /// <param name="target">The name of the machine to connect to.</param>
  23:         public InternetInformationServer ( string target ) : base ( string.Format ( @"\\{0}\root\MicrosoftIISV2", target ) ) { }
  24:  
  25:         /// <summary>
  26:         /// Create a new website on the IIS instance.
  27:         /// </summary>
  28:         /// <param name="serverComment">The server comment.</param>
  29:         /// <param name="rootPath">The path to the root virtual directory of the web site.</param>
  30:         /// <returns>A new WebSite instance.</returns>
  31:         public WebSite CreateWebSite ( string serverComment, string rootPath )
  32:         {
  33:             return this.CreateWebSite ( serverComment, rootPath, new ServerBinding[] { new ServerBinding () } );
  34:         }
  35:  
  36:         /// <summary>
  37:         /// Create a new web site on the IIS instance.
  38:         /// </summary>
  39:         /// <param name="serverComment">The server comment.</param>
  40:         /// <param name="rootPath">The path to the root virtual directory of the web site.</param>
  41:         /// <param name="serverBindings">A list of ServerBinding objects to apply to the web site.</param>
  42:         /// <returns></returns>
  43:         public WebSite CreateWebSite ( string serverComment, string rootPath, ServerBinding[] serverBindings )
  44:         {
  45:             const string METHOD = "CreateNewSite";
  46:  
  47:             // create a list of serverbindings...
  48:             List<ManagementObject> bindings = new List<ManagementObject> ();
  49:             Array.ForEach ( serverBindings, binding => bindings.Add ( CreateServerBinding ( binding ) ) );
  50:  
  51:             // create the site..
  52:             ManagementObject w3svc = new ManagementObject ( this.Scope, new ManagementPath ( @"IISWebService='W3SVC'" ), null );
  53:             ManagementBaseObject parameters = w3svc.GetMethodParameters ( METHOD );
  54:  
  55:             parameters["ServerComment"] = serverComment;
  56:             parameters["ServerBindings"] = bindings.ToArray ();
  57:             parameters["PathOfRootVirtualDir"] = rootPath;
  58:  
  59:             ManagementBaseObject result = w3svc.InvokeMethod ( METHOD, parameters, null );
  60:             Match m = Regex.Match ( (string)result.Properties["ReturnValue"].Value, "'(.*?)'" );
  61:  
  62:             return new WebSite ( this.Scope, serverComment, m.Groups[1].Value );
  63:         }
  64:  
  65:         /// <summary>
  66:         /// Create a new application pool on the IIS instance
  67:         /// </summary>
  68:         /// <param name="name">The name of the application pool.</param>
  69:         /// <returns></returns>
  70:         public ApplicationPool CreateAppliationPool ( string name )
  71:         {
  72:             ApplicationPool appPool = new ApplicationPool ( this.Scope );
  73:             appPool.Name = string.Format ( "W3SVC/AppPools/{0}", name );
  74:  
  75:             ManagementObject poolTemplate = CreateManagementObject ( "IIsApplicationPoolSetting" );
  76:             poolTemplate.Properties["Name"].Value = appPool.Name;
  77:             poolTemplate.Put ();
  78:  
  79:             return appPool;
  80:         }
  81:  
  82:         private ManagementObject CreateServerBinding ( ServerBinding binding )
  83:         {
  84:             ManagementObject managementObject = CreateManagementObject ( "ServerBinding" );
  85:             managementObject.Properties["Hostname"].Value = binding.HostName;
  86:             managementObject.Properties["IP"].Value = binding.IPAddress;
  87:             managementObject.Properties["Port"].Value = binding.Port;
  88:  
  89:             return managementObject;
  90:         }
  91:  
  92:         /// <summary>
  93:         /// Iterator of all the sites on the IIS instance.
  94:         /// </summary>
  95:         public IEnumerable<WebSite> Sites
  96:         {
  97:             get
  98:             {
  99:                 ObjectQuery query = new ObjectQuery ( "SELECT * FROM IISWebServerSetting" );
 100:                 foreach ( ManagementObject item in this[query] )
 101:                 {
 102:                     yield return new WebSite ( this.Scope, item["ServerComment"].ToString (), item["Name"].ToString () );
 103:                 }
 104:             }
 105:         }
 106:  
 107:  
 108:         /// <summary>
 109:         /// Get a reference to an appliation pool on the IIS instance.
 110:         /// </summary>
 111:         /// <param name="applicationPoolId">The id of the application pool.</param>
 112:         /// <returns>An ApplicationPool object or null if not found.</returns>
 113:         public ApplicationPool GetApplicationPool ( string applicationPoolId )
 114:         {
 115:             ObjectQuery query = new ObjectQuery ( string.Format ( "SELECT * FROM IIsApplicationPoolSetting WHERE Name='W3SVC/AppPools/{0}'", applicationPoolId ) );
 116:             foreach ( ManagementObject item in this[query] )
 117:             {
 118:                 return new ApplicationPool ( this.Scope, (string)item.Properties["Name"].Value );
 119:             }
 120:             return null;
 121:         }
 122:  
 123:  
 124:         /// <summary>
 125:         /// Get a reference to an existing web site on the IIS instance based on the server comment
 126:         /// </summary>
 127:         /// <param name="serverComment">The server comment of the site you want to get.</param>
 128:         /// <returns>A WebSite object or null if not found.</returns>
 129:         /// <remarks>
 130:         /// This method is really a special purpose method - the server comment is not required to be unique in most cases.  But,
 131:         /// in my case it is.  If you can't guarentee uniqueness then you will either need to make this return the a list, or
 132:         /// uses the site id rather then the server comment.
 133:         /// </remarks>
 134:         public WebSite GetWebSite ( string serverComment )
 135:         {
 136:             ObjectQuery query = new ObjectQuery ( "SELECT * FROM IISWebServerSetting WHERE ServerComment = '" + serverComment + "'" );
 137:             foreach ( ManagementObject item in this[query] )
 138:             {
 139:                 return new WebSite ( this.Scope, item["ServerComment"].ToString (), item["Name"].ToString () );
 140:             }
 141:  
 142:             return null;
 143:         }
 144:  
 145:     }
 146: }

Please not the comments on the GetWebSite method…  In the infrastructure I’m working in, I can guarentee that the server comment is unique for a given web site.  In many cases, this can’t be guarenteed – so, really this method should be changed either to return a list of WebSite objects, or it should be refactored to take the site id – which is unique.

There are two overloads for the CreateWebSite method – one that takes a list of ServerBindings and one that doesn’t.  You’ll notice that the overload that does not take a list of bindings simply delegates to the method that does, and passes in a default binding object.

Here are the ApplicationPool, WebSite, and ServerBinding objects as I currently have them:

WebSite:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Management;
   6:  
   7: namespace FireAnt.IIS.Util
   8: {
   9:     public class WebSite : WmiObjectBase
  10:     {
  11:         private readonly string IdentityQuery;
  12:  
  13:         internal WebSite ( ManagementScope scope, string serverComment, string siteName ) : base(scope)
  14:         {
  15:             this.ServerComment = serverComment;
  16:             this.Name = siteName;
  17:             this.IdentityQuery = string.Format ( "SELECT * FROM IISWebServer WHERE Name = '{0}'", this.Name );
  18:         }
  19:  
  20:         public string ServerComment { get; set; }
  21:         public string Name { get; set; }
  22:  
  23:         //public void Continue ()
  24:         //{
  25:         //}
  26:  
  27:         //public void Pause ()
  28:         //{
  29:         //}
  30:  
  31:         public void Start ()
  32:         {
  33:             ServerState state = CurrentState;
  34:             if ( state == ServerState.Stopped )
  35:             {
  36:                 ObjectQuery query = new ObjectQuery ( IdentityQuery );
  37:                 foreach ( ManagementObject site in this[query] )
  38:                 {
  39:                     site.InvokeMethod ( "Start", null );
  40:                 }
  41:             }
  42:  
  43:         }
  44:  
  45:         public void Stop ()
  46:         {
  47:             ServerState state = CurrentState;
  48:             if ( state == ServerState.Started )
  49:             {
  50:                 ObjectQuery query = new ObjectQuery ( IdentityQuery );
  51:                 foreach ( ManagementObject site in this[query] )
  52:                 {
  53:                     site.InvokeMethod ( "Stop", null );
  54:                 }
  55:             }
  56:         }
  57:  
  58:         public WebVirtualDirectory DirectorySettings
  59:         {
  60:             get { return new WebVirtualDirectory ( this.Scope, this.Name ); }
  61:         }
  62:  
  63:         public ServerState CurrentState
  64:         {
  65:             get
  66:             {
  67:                 ObjectQuery query = new ObjectQuery ( string.Format ( "SELECT ServerState FROM IISWebServer WHERE Name = '{0}'", this.Name ) );
  68:  
  69:                 foreach ( ManagementObject item in this[query] )
  70:                 {
  71:                     return (ServerState)Convert.ToInt32 ( item["ServerState"] );
  72:                 }
  73:                 throw new Exception ( "Can't determine server's current state" );
  74:             }
  75:         }
  76:  
  77:         public override string ToString ()
  78:         {
  79:             return string.Format ( "{0} ({1})", ServerComment, Name );
  80:         }
  81:     }
  82:  
  83:     public enum ServerState
  84:     {
  85:         Starting = 1,
  86:         Started = 2,
  87:         Stopping = 3,
  88:         Stopped = 4,
  89:         Pausing = 5,
  90:         Paused = 6,
  91:         Continuing = 7,
  92:     }
  93: }

ApplicationPool:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Management;
   6:  
   7: namespace FireAnt.IIS.Util
   8: {
   9:     public class ApplicationPool : WmiObjectBase
  10:     {
  11:         internal ApplicationPool ( ManagementScope scope )
  12:             : base ( scope )
  13:         {
  14:         }
  15:  
  16:         internal ApplicationPool ( ManagementScope scope, string name )
  17:             : base ( scope )
  18:         {
  19:             this.Name = name;
  20:         }
  21:  
  22:         public string Name { get; internal set; }
  23:     }
  24: }

ServerBinding:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5:  
   6: namespace FireAnt.IIS.Util
   7: {
   8:     public class ServerBinding
   9:     {
  10:         public ServerBinding () : this ( string.Empty, string.Empty, "80" ) { }
  11:         public ServerBinding ( string hostName, string ipAddress, string port )
  12:         {
  13:             this.HostName = hostName;
  14:             this.IPAddress = ipAddress;
  15:             this.Port = port;
  16:         }
  17:  
  18:         public string HostName { get; set; }
  19:         public string IPAddress { get; set; }
  20:         public string Port { get; set; }
  21:     }
  22: }

Again these are relatively simple classes – but, again they have room to expand.

WebSite does have some properties and methods that maybe of interest.  It allows you to start and stop the individual site – so you don’t have to bring down the entire IIS server, just to configure your site.  It also allows you to get a reference to it’s root virtual directory so you can change settings – such as the framework version the site should use.  Other properties of the root directory can be set via a dictionary of name value pairs.  This was for me the most convenient method, since I was using xml files to store my configuration settings:

 

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Management;
   6: using System.Text.RegularExpressions;
   7:  
   8: namespace FireAnt.IIS.Util
   9: {
  10:     public class WebVirtualDirectory : WmiObjectBase
  11:     {
  12:         internal WebVirtualDirectory ( ManagementScope scope, string siteName ) : base(scope)
  13:         {
  14:             this.Path = string.Format ( "IIsWebVirtualDirSetting='{0}/ROOT'", siteName );
  15:         }
  16:  
  17:         private string Path { get; set; }
  18:  
  19:         public void SetFrameworkVersion ( string version )
  20:         {
  21:             ManagementObject root = this.GetInstance ( this.Path );
  22:             foreach ( PropertyData property in root.Properties )
  23:             {
  24:                 if ( property.Name == "ScriptMaps" )
  25:                 {
  26:                     ManagementBaseObject[] scriptMaps = (ManagementBaseObject[])property.Value;
  27:                     foreach ( ManagementBaseObject scriptMap in scriptMaps )
  28:                     {
  29:                         string value = (string)scriptMap["ScriptProcessor"];
  30:                         if ( value.ToLower ().Contains ( "framework" ) )
  31:                         {
  32:                             if ( !value.Contains ( version ) )
  33:                             {
  34:                                 string currentVersion = Regex.Match ( value, @"(v\d+\.\d+\.\d+)" ).Value;
  35:                                 value = value.Replace ( currentVersion, version );
  36:                                 scriptMap.SetPropertyValue ( "ScriptProcessor", value );
  37:                             }
  38:                             else
  39:                             {
  40:                                 return;
  41:                             }
  42:                         }
  43:                     }
  44:  
  45:                     property.Value = scriptMaps;
  46:                     root.Put ();
  47:                     break;
  48:                 }
  49:             }
  50:         }
  51:  
  52:         public void ApplySettings ( Dictionary<string, object> properties )
  53:         {
  54:             ManagementObject root = this.GetInstance ( this.Path );
  55:             ApplySettings ( root, properties );
  56:         }
  57:     }
  58: }

Here is some code that demonstrates how you might use the above library (Air-Code Warning!):

 

   1: static class Program
   2: {
   3:     internal static string EnvironmentName { get; set; }
   4:     static int Main ( string[] args )
   5:     {
   6:         EnvironmentName = args[0];
   7:         InternetInformationServer iis = new InternetInformationServer ( Program.EnvironmentName );
   8:         WebSite webSite = iis.GetWebSite ( ServerComment );
   9:         ShutdownSite ( webSite );
  10:  
  11:         // DO SOME STUFF
  12:  
  13:         // create the website
  14:          if ( webSite == null )
  15:          {
  16:                 ApplicationPool pool = iis.GetApplicationPool ( poolName );
  17:                  if ( pool == null )
  18:                      iis.CreateAppliationPool ( poolName );
  19:         
  20:              webSite = iis.CreateWebSite ( ServerComment, webRootPath );
  21:                 
  22:              // apply any virtual root properties here
  23:              WebVirtualDirectory virtualDirectory = webSite.DirectorySettings;
  24:              virtualDirectory.SetFrameworkVersion ( "v2.0.50727" );         }
  25:         
  26:          // start up the website...
  27:          webSite.Start ();
  28:     }
  29:  
  30:     private static void ShutdownSite ( WebSite webSite )
  31:      {
  32:          if ( webSite != null )
  33:          {
  34:              webSite.Stop ();
  35:          }
  36:      }
  37: }

Anyway,  I apologize if the level of detail here is less then expected – but I had to throw this together fairly quickly.  I wanted to get it out there in the hopes that someone might find some of this useful.

February 21, 2009

Exploring SQL Server Schema Information With ADO.NET

Filed under: ADO.NET,C# — Tom Shelton @ 7:00 pm

If you read my article on finding SQL Server instances, you’ll know that I mentioned that I planned on writing a series of articles around a code generator that I have been playing with off and on for the last couple of years.  Since this generator was designed to create class definitions around tables, views, and procedures in an SQL Server database, I needed to figure out and write the code to get schema information from a given connection.  As it turns out, ADO.NET provides a fairly robust mechanism for implementers of data providers to return this information from the underlying data store.  In this article, I will be focusing on the information available from the System.Data.SqlClient namespace – but, techniques shown here apply to all of the data providers built into the .NET framework.  I have similar code using System.Data.OracleClient that I can provide if it is of interest.

I’ll start off simple and create a small C# console application that can list all of the databases in an SQL Server instance.

 

   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4:  
   5: namespace SchemaInfo
   6: {
   7:     class Program
   8:     {
   9:         static void Main ( string[] args )
  10:         {
  11:             // build a connection string to a sql server instance
  12:             SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder ();
  13:             connectionBuilder.DataSource = "SAURON";
  14:             connectionBuilder.IntegratedSecurity = true;
  15:  
  16:             // now, lets list all of the databases in this instance
  17:             using ( SqlConnection connection = new SqlConnection ( connectionBuilder.ConnectionString ) )
  18:             {
  19:                 connection.Open ();
  20:  
  21:                 // get the database information
  22:                 DataTable databases = connection.GetSchema ( SqlClientMetaDataCollectionNames.Databases );
  23:  
  24:                 // print out the connections
  25:                 foreach ( DataColumn column in databases.Columns )
  26:                 {
  27:                     Console.Write ( "{0,-25}", column.ColumnName );
  28:                 }
  29:                 Console.WriteLine ();
  30:                 
  31:                 // print out the rows...
  32:                 foreach ( DataRow database in databases.Rows )
  33:                 {
  34:                     for ( int i = 0; i < database.ItemArray.Length; i++ )
  35:                     {
  36:                         Console.Write ( "{0,-25}", database.ItemArray[i] );
  37:                     }
  38:                     Console.WriteLine ();
  39:                 }
  40:             }
  41:         }
  42:     }
  43: }

schema001

The key to the above codes success is the SqlConnection.GetSchema method.  By passing in a string that represents the name of one of the provider’s supported schema collections, you can get a DataTable back containing the requested information.  There are essentially two types of schema collections – Common Schema Collections and Provider Specific Schema Collections.   As the names imply, the common schema collections are the schema collections implemented by all of the .NET framework providers, where as the provider specific collections apply to a specific provider.

You can get a list of the schema collections supported by your provider by calling the GetSchema method with no arguments.  Here is the above example, with the schema collection name commented out:

 

   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4: using System.Windows.Forms;
   5:  
   6: namespace SchemaInfo
   7: {
   8:     class Program
   9:     {
  10:         static void Main ( string[] args )
  11:         {
  12:             // build a connection string to a sql server instance
  13:             SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder ();
  14:             connectionBuilder.DataSource = "SAURON";
  15:             connectionBuilder.IntegratedSecurity = true;
  16:  
  17:             // now, lets list all of the databases in this instance
  18:             using ( SqlConnection connection = new SqlConnection ( connectionBuilder.ConnectionString ) )
  19:             {
  20:                 connection.Open ();
  21:  
  22:                 // get the database information
  23:                 DataTable databases = connection.GetSchema ( /*SqlClientMetaDataCollectionNames.Databases*/ );
  24:  
  25:                 // print out the connections
  26:                 foreach ( DataColumn column in databases.Columns )
  27:                 {
  28:                     Console.Write ( "{0,-25}", column.ColumnName );
  29:                 }
  30:                 Console.WriteLine ();
  31:  
  32:                 // print out the rows...
  33:                 foreach ( DataRow database in databases.Rows )
  34:                 {
  35:                     for ( int i = 0; i < database.ItemArray.Length; i++ )
  36:                     {
  37:                         Console.Write ( "{0,-25}", database.ItemArray[i] );
  38:                     }
  39:                     Console.WriteLine ();
  40:                 }
  41:             }
  42:         }
  43:     }
  44: }

schema002

You can find a lot of information on using the GetSchema method and the various schema collections here.

So, now that we’ve introduced the concept GetSchema and schema collections, let’s do something a little more complex.  Let’s build a database explorer type control.  Something like a very simplified version of the Object Explorer in SQL Server Management Studio.

 

1.

Start Visual Studio, and start a new blank solution.  Do this by selecting File -> New -> Project and then selecting "Other Project Types -> Visual Studio Solutions" in the "Project Types" tree view in the "New Project" dialog.

  schema003
   

2.

Add a Windows Forms application to the blank project to serve as a test harness for the control.  Right click on the solution in "Solution Explorer" and select "Add -> New Project…".

  schema004
   

3.

Now, following the same steps as before, add a new "Windows Forms Control Library" to the solution.

  schema005
   

4.

In the windows forms project add a reference to the control library project.  To do this, right click on the windows form project in the "Solution Explorer" and select "Add Reference…".  Select the control library on the Projects tab of the "Add Reference" dialog.  Select OK.

  schema006
   

5.

Rename the default UserControl1 in the windows control library to something more appropriate.  The easiest way to do this is simply right click on the control in the "Solution Explorer" and select "Rename".  I called mine "DBExplorer".  Compile the solution.

   

6.

Now, we create a simple interface so we can see the control in action.  I added a SplitContainer to the default Form1 in the windows forms application.  Then added an instance of the DBExplorer control to Panel1 of the SplitContainer, and set it’s Dock property to Fill.  I named the DBExplorer instance uxDBExplorer.

  schema007
 

Not much to look at – but, this is just a test harness :)

   

7.

Go back to the control library project and add a treeview control to the DBExplorer interface.  Set it’s Dock property to fill and give it an appropriate name.  I gave mine the name of uxExplorer.  Compile the solution.

   

8.

Here is the code for the DBExplorer user control:

 
   1: using System;
   2: using System.Data;
   3: using System.Data.SqlClient;
   4: using System.Windows.Forms;
   5:  
   6: namespace DatabaseExplorer
   7: {
   8:     public partial class DBExplorer : UserControl
   9:     {
  10:         public DBExplorer ()
  11:         {
  12:             InitializeComponent ();
  13:         }
  14:  
  15:         public string ConnectionString { get; set; }
  16:  
  17:         private void DBExplorer_Load ( object sender, EventArgs e )
  18:         {
  19:             if ( !this.DesignMode && !string.IsNullOrEmpty ( this.ConnectionString ) )
  20:             {
  21:                 // create the first node...
  22:                 this.uxExplorer.Nodes.Add ( new ConnectionNode ( new SqlConnectionStringBuilder ( this.ConnectionString ) ) );
  23:             }
  24:         }
  25:  
  26:         private void uxExplorer_BeforeExpand ( object sender, TreeViewCancelEventArgs e )
  27:         {
  28:             if ( e.Node is DataSourceNodeBase )
  29:             {
  30:                 string oldText = e.Node.Text;
  31:                 try
  32:                 {
  33:                     e.Node.Text = string.Format ( "{0} (expanding...)", e.Node.Text );
  34:                     this.uxExplorer.Refresh ();
  35:                     this.uxExplorer.BeginUpdate ();
  36:                     ( (DataSourceNodeBase)e.Node ).Load ();
  37:                 }
  38:                 catch ( Exception ex )
  39:                 {
  40:                     MessageBox.Show ( ex.Message );
  41:                 }
  42:                 finally
  43:                 {
  44:                     this.uxExplorer.EndUpdate ();
  45:                     e.Node.Text = oldText;
  46:                 }
  47:             }
  48:         }
  49:  
  50:         #region Private Classes
  51:         /// <summary>
  52:         /// Used as a simple placeholder before an expand...
  53:         /// </summary>
  54:         private class DummyNode : TreeNode
  55:         {
  56:             public DummyNode ()
  57:                 : base ( "DUMMY" )
  58:             {
  59:                 this.Name = "DUMMY";
  60:             }
  61:         }
  62:  
  63:         /// <summary>
  64:         /// Provides a base class for a node that must connect to a
  65:         /// datasource.
  66:         /// </summary>
  67:         private abstract class DataSourceNodeBase : TreeNode
  68:         {
  69:             protected DataSourceNodeBase ( string text, SqlConnectionStringBuilder builder )
  70:                 : base ( text )
  71:             {
  72:                 this.ConnectionStringBuilder = builder;
  73:                 this.Nodes.Add ( new DummyNode () );
  74:             }
  75:             
  76:             /// <summary>
  77:             /// The connection string used to connect to the datasource
  78:             /// </summary>
  79:             protected SqlConnectionStringBuilder ConnectionStringBuilder { get; set; }
  80:  
  81:             /// <summary>
  82:             /// Loads child nodes if it hasn't been loaded before
  83:             /// </summary>
  84:             public virtual void Load ()
  85:             {
  86:                 if ( this.Nodes.ContainsKey ( "DUMMY" ) )
  87:                 {
  88:                     this.Reload ();
  89:                 }
  90:             }
  91:  
  92:             /// <summary>
  93:             /// Refreshes children from the datasource
  94:             /// </summary>
  95:             public virtual void Reload ()
  96:             {
  97:                 this.Nodes.Clear ();
  98:                 this.LoadDatabaseObjects ();
  99:             }
 100:  
 101:             /// <summary>
 102:             /// Loads subnodes from the datasource
 103:             /// </summary>
 104:             protected abstract void LoadDatabaseObjects ();
 105:         }
 106:  
 107:         /// <summary>
 108:         /// Root node for a datasource connection
 109:         /// </summary>
 110:         private class ConnectionNode : DataSourceNodeBase
 111:         {
 112:             public ConnectionNode ( SqlConnectionStringBuilder builder ) : base ( builder.DataSource, builder ) { }
 113:  
 114:             protected override void LoadDatabaseObjects ()
 115:             {
 116:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 117:                 {
 118:                     connection.Open ();
 119:                     DataTable databases = connection.GetSchema ( SqlClientMetaDataCollectionNames.Databases );
 120:                     foreach ( DataRow database in databases.Rows )
 121:                     {
 122:                         SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder ( this.ConnectionStringBuilder.ConnectionString );
 123:                         builder.InitialCatalog = (string)database["database_name"];
 124:                         this.Nodes.Add ( new DatabaseNode ( builder ) );
 125:                     }
 126:                 }
 127:             }
 128:         }
 129:  
 130:         /// <summary>   
 131:         /// Node whose text property contains a database.
 132:         /// </summary>
 133:         private class DatabaseNode : TreeNode
 134:         {
 135:             public DatabaseNode ( SqlConnectionStringBuilder builder )
 136:                 : base ( builder.InitialCatalog )
 137:             {
 138:                 this.Nodes.Add ( new TableRootNode ( builder ) );
 139:                 this.Nodes.Add ( new ViewRootNode ( builder ) );
 140:                 this.Nodes.Add ( new StoredProcedureRootNode ( builder ) );
 141:             }
 142:         }
 143:  
 144:         /// <summary>
 145:         /// Root node for all base tables
 146:         /// </summary>
 147:         private class TableRootNode : DataSourceNodeBase
 148:         {
 149:             public TableRootNode ( SqlConnectionStringBuilder builder ) : base ( "Tables", builder ) { }
 150:  
 151:             protected override void LoadDatabaseObjects ()
 152:             {
 153:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 154:                 {
 155:                     connection.Open ();
 156:                     DataTable tables = connection.GetSchema ( SqlClientMetaDataCollectionNames.Tables, new string[] { null, null, null, "BASE TABLE" } );
 157:                     foreach ( DataRow table in tables.Rows )
 158:                     {
 159:                         this.Nodes.Add ( new TableNode ( string.Format ( "{0}.{1}", table["TABLE_SCHEMA"], table["TABLE_NAME"] ), this.ConnectionStringBuilder ) );
 160:                     }
 161:                 }
 162:             }
 163:         }
 164:  
 165:         /// <summary>
 166:         /// Root node for all views
 167:         /// </summary>
 168:         private class ViewRootNode : DataSourceNodeBase
 169:         {
 170:             public ViewRootNode ( SqlConnectionStringBuilder builder ) : base ( "Views", builder ) { }
 171:  
 172:             protected override void LoadDatabaseObjects ()
 173:             {
 174:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 175:                 {
 176:                     connection.Open ();
 177:                     DataTable tables = connection.GetSchema ( SqlClientMetaDataCollectionNames.Tables, new string[] { null, null, null, "VIEW" } );
 178:                     foreach ( DataRow table in tables.Rows )
 179:                     {
 180:                         this.Nodes.Add ( new TableNode ( string.Format ( "{0}.{1}", table["TABLE_SCHEMA"], table["TABLE_NAME"] ), this.ConnectionStringBuilder ) );
 181:                     }
 182:                 }
 183:             }
 184:         }
 185:  
 186:         /// <summary>
 187:         /// Node to store information about tables and views.
 188:         /// </summary>
 189:         private class TableNode : DataSourceNodeBase
 190:         {
 191:             public TableNode ( string text, SqlConnectionStringBuilder builder )
 192:                 : base ( text, builder )
 193:             {
 194:                 
 195:             }
 196:  
 197:             protected override void LoadDatabaseObjects ()
 198:             {
 199:                 // TODO: GATHER FIELD INFORMATION
 200:             }
 201:  
 202:         }
 203:  
 204:         /// <summary>
 205:         /// Node that when expanding, will get stored procedure information
 206:         /// </summary>
 207:         private class StoredProcedureRootNode : DataSourceNodeBase
 208:         {
 209:             public StoredProcedureRootNode ( SqlConnectionStringBuilder builder ) : base ( "Stored Procedures", builder ) { }
 210:  
 211:             protected override void LoadDatabaseObjects ()
 212:             {
 213:                 using ( SqlConnection connection = new SqlConnection ( this.ConnectionStringBuilder.ConnectionString ) )
 214:                 {
 215:                     connection.Open ();
 216:                     DataTable procedures = connection.GetSchema ( SqlClientMetaDataCollectionNames.Procedures, new string[] { null, null, null, "PROCEDURE" } );
 217:                     foreach ( DataRow procedure in procedures.Rows )
 218:                     {
 219:  
 220:                         this.Nodes.Add ( new ProcedureNode ( string.Format ( "{0}.{1}", procedure["SPECIFIC_SCHEMA"], procedure["SPECIFIC_NAME"] ), this.ConnectionStringBuilder ) );
 221:                     }
 222:                 }
 223:             }
 224:         }
 225:  
 226:         /// <summary>
 227:         /// Node to store information about a stored procedures
 228:         /// </summary>
 229:         private class ProcedureNode : DataSourceNodeBase
 230:         {
 231:             public ProcedureNode ( string text, SqlConnectionStringBuilder builder )
 232:                 : base ( text, builder )
 233:             {
 234:             }
 235:  
 236:             protected override void LoadDatabaseObjects ()
 237:             {
 238:                 // TODO: GATHER PARAMETER INFORMATION...
 239:             }
 240:         }
 241:         #endregion
 242:     }
 243: }

   

9.

In the test forms designer, set the ConnectionString property of the DBExplorer instance to point to a SQL Server Instance.  Here is the result when I point it to a local instance, and run the project:

  schema008

I purposefully left the implementation of gathering field and parameter info to the reader – but, I hope that this was enough to introduce the key concepts of using the GetSchema method of the SqlConnection class to dynamically discover schema infromation.  In the entity generator that I have been discussing, I use a variant of this exact control.  The major differences being that it supports multiple connection nodes and displays images for the various database objects.

January 6, 2009

Programmatically Discover SQL Server Instances

Filed under: C# — Tom Shelton @ 5:29 pm

I’ve been working on a little pet project of mine again lately.  I have started updating an fixing up my old simple class generator again.  It’s nothing special – just something I created for my own use one day, and have sort of been poking at here and there for the last couple of years.  But, looking through the code – I did see several areas that I thought might make a nice series of articles.  This first one will be to document the method that the entity generator uses to discover SQL Server instances on my network and local machines.

I have encapsulated this code into a reusable control library – that someday, I might actually finish!  But, in it’s current state the ServerInfoControl looks like this:

connectToServer004

Why a control?  Well, I wanted to be able to use it in a couple of different applications in different types of forms – so I decided to make it a control.  It’s unfinished, the remember password functionality doesn’t work – and for some reason the controls ability to remember previous selected connections (implemented as user settings) has decided to stop working.  Both issues, I fully intend to look into sometime :)

Here is what it looks like in action in the entity generator:

connectToServer002

Not to shabby, if I do say so myself!  Here is the code for the ServerInfoControl:

   1: using System;
   2: using System.Windows.Forms;
   3: using System.Data.SqlClient;
   4:
   5: namespace FireAnt.Controls.Database
   6: {
   7:     public partial class ServerInfoControl : UserControl
   8:     {
   9:         private const string BrowseInstanceServerName = "<Browse for More...>";
  10:
  11:         public event ConnectionValidDelegate ConnectionValid;
  12:
  13:         public ServerInfoControl ()
  14:         {
  15:             InitializeComponent ();
  16:         }
  17:
  18:         public string ConnectionString
  19:         {
  20:             get
  21:             {
  22:                 return BuildConnectionString ();
  23:             }
  24:         }
  25:
  26:         private string BuildConnectionString ()
  27:         {
  28:             string connectionString = string.Empty;
  29:             if ( this.uxServers.SelectedItem != null && ( (ServerInstance)this.uxServers.SelectedItem ).Instance != BrowseInstanceServerName )
  30:             {
  31:                 SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder ();
  32:                 connectionBuilder.DataSource = ( (ServerInstance)this.uxServers.SelectedItem ).FullName;
  33:                 ( (AuthenticationBase)this.uxAuthentication.SelectedItem ).AlterConnectionString ( connectionBuilder );
  34:                 connectionString = connectionBuilder.ToString ();
  35:             }
  36:             return connectionString;
  37:         }
  38:
  39:         private void ServerInfoControl_Load ( object sender, EventArgs e )
  40:         {
  41:             if ( !this.DesignMode )
  42:             {
  43:                 ServerInfoControlSettings.Default.Reload ();
  44:
  45:                 // add all the instances we know of to the combobox...
  46:                 foreach ( ServerInstance instance in ServerInfoControlSettings.Default.Servers )
  47:                 {
  48:                     this.uxServers.Items.Add ( instance );
  49:                 }
  50:
  51:                 // add the default instance at the end
  52:                 this.uxServers.Items.Add ( new ServerInstance ( BrowseInstanceServerName ) );
  53:
  54:                 // if there are others, set the first one in the list...
  55:                 if ( this.uxServers.Items.Count > 1 )
  56:                 {
  57:                     this.uxServers.SelectedIndex = 0;
  58:                 }
  59:
  60:                 // add the authmodes, and set the index
  61:                 this.uxAuthentication.Items.AddRange ( new AuthenticationBase[] { new WindowsAuthentication ( this ), new SqlServerAuthentication ( this ) } );
  62:                 this.uxAuthentication.SelectedIndex = 0;
  63:             }
  64:         }
  65:
  66:         private void uxAuthentication_SelectedIndexChanged ( object sender, EventArgs e )
  67:         {
  68:             this.uxUserName.Enabled =
  69:                 this.uxPassword.Enabled =
  70:                 this.uxRememberPassword.Enabled = this.uxAuthentication.SelectedItem is SqlServerAuthentication;
  71:             this.TryRaiseConnectionValidEvent ();
  72:         }
  73:
  74:         private void uxServers_SelectedIndexChanged ( object sender, EventArgs e )
  75:         {
  76:             if ( this.uxServers.SelectedItem != null )
  77:             {
  78:                 ServerInstance instance = (ServerInstance)this.uxServers.SelectedItem;
  79:                 if ( instance.Server == BrowseInstanceServerName )
  80:                 {
  81:                     using ( BrowseForServerDialog bfsd = new BrowseForServerDialog () )
  82:                     {
  83:                         if ( bfsd.ShowDialog ( this ) == DialogResult.OK )
  84:                         {
  85:
  86:                             this.AddToComboBox ( bfsd.ServerInstance );
  87:                             this.uxServers.SelectedItem = bfsd.ServerInstance;
  88:                             this.SaveServerInstance ( bfsd.ServerInstance );
  89:                             this.TryRaiseConnectionValidEvent ();
  90:                         }
  91:                         else
  92:                         {
  93:                             this.uxServers.SelectedItem = null;
  94:                             this.RaiseConnectionValidEvent ( false );
  95:                         }
  96:                     }
  97:                 }
  98:                 else
  99:                 {
 100:                     this.TryRaiseConnectionValidEvent ();
 101:                 }
 102:             }
 103:         }
 104:
 105:         private void TryRaiseConnectionValidEvent ()
 106:         {
 107:             bool valid = (this.uxServers.SelectedItem != null) ?
 108:                 (this.uxAuthentication.SelectedItem is WindowsAuthentication) ?
 109:                 true :
 110:                 (this.uxUserName.Text.Length > 0 && this.uxPassword.Text.Length > 0): false;
 111:             this.RaiseConnectionValidEvent ( valid );
 112:         }
 113:
 114:         private void RaiseConnectionValidEvent ( bool valid )
 115:         {
 116:             if ( this.ConnectionValid != null )
 117:                 this.ConnectionValid ( this, new ConnectionValidEventArgs ( valid ) );
 118:         }
 119:         private void AddToComboBox ( ServerInstance instance )
 120:         {
 121:             foreach ( ServerInstance server in this.uxServers.Items )
 122:             {
 123:                 if ( server.Equals ( instance ) )
 124:                     return;
 125:             }
 126:             this.uxServers.Items.Insert ( 0, instance );
 127:
 128:         }
 129:
 130:         private void SaveServerInstance ( ServerInstance instance )
 131:         {
 132:             foreach ( ServerInstance server in ServerInfoControlSettings.Default.Servers )
 133:             {
 134:                 if ( server.Equals ( instance ) )
 135:                     return;
 136:             }
 137:
 138:             ServerInfoControlSettings.Default.Servers.Insert ( 0, instance );
 139:             ServerInfoControlSettings.Default.Save ();
 140:         }
 141:
 142:         private abstract class AuthenticationBase
 143:         {
 144:             public AuthenticationBase ( ServerInfoControl parent )
 145:             {
 146:                 this.Parent = parent;
 147:             }
 148:
 149:             protected ServerInfoControl Parent { get; set; }
 150:
 151:             public abstract void AlterConnectionString ( SqlConnectionStringBuilder connectionBuilder );
 152:         }
 153:
 154:         private class WindowsAuthentication : AuthenticationBase
 155:         {
 156:             public WindowsAuthentication ( ServerInfoControl parent ) : base ( parent ) { }
 157:
 158:             public override void AlterConnectionString ( SqlConnectionStringBuilder connectionBuilder )
 159:             {
 160:                 connectionBuilder.IntegratedSecurity = true;
 161:             }
 162:
 163:             public override string ToString ()
 164:             {
 165:                 return "Windows Authentication";
 166:             }
 167:         }
 168:
 169:         private class SqlServerAuthentication : AuthenticationBase
 170:         {
 171:             public SqlServerAuthentication ( ServerInfoControl parent ) : base ( parent ) { }
 172:
 173:             public override void AlterConnectionString ( SqlConnectionStringBuilder connectionBuilder )
 174:             {
 175:                 connectionBuilder.UserID = this.Parent.uxUserName.Text;
 176:                 connectionBuilder.Password = this.Parent.uxPassword.Text;
 177:             }
 178:
 179:             public override string ToString ()
 180:             {
 181:                 return "SQL Server Authentication";
 182:             }
 183:         }
 184:
 185:         private void CommonTextChanged ( object sender, EventArgs e )
 186:         {
 187:             this.TryRaiseConnectionValidEvent ();
 188:         }
 189:     }
 190: }

Not sure there is anything here that is interesting – except for maybe the TryRaiseConnectionValidEvent, which is in serious need of refactoring (multiple nested ternary operators!  What was I thinking!).

So, lets move to the more interesting part of the code.  That is found in the BrowseServerDialog form.  This form is internal to the control library, and is shown when you select the “<Browse For More…>” option in the drop down of the control.   Here it is in action:

connectToServer003

And here is the code for this dialog box:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ComponentModel;
   4: using System.Data;
   5: using System.Drawing;
   6: using System.Text;
   7: using System.Windows.Forms;
   8: using Microsoft.Win32;
   9:
  10: namespace FireAnt.Controls.Database
  11: {
  12:     internal partial class BrowseForServerDialog : Form
  13:     {
  14:         private ServerInstance selectedInstance = null;
  15:
  16:         public BrowseForServerDialog ()
  17:         {
  18:             InitializeComponent ();
  19:         }
  20:
  21:         private void BrowseForServerDialog_Load ( object sender, EventArgs e )
  22:         {
  23:             if ( !this.DesignMode )
  24:             {
  25:                 this.FillTreeView ( this.uxLocalServers, ServerInstance.GetLocalServerList () );
  26:                 this.uxLoadNetworkServers.RunWorkerAsync ();
  27:             }
  28:         }
  29:
  30:         public ServerInstance ServerInstance
  31:         {
  32:             get { return this.selectedInstance; }
  33:         }
  34:
  35:
  36:         private void TreeView_AfterSelect ( object sender, TreeViewEventArgs e )
  37:         {
  38:             this.selectedInstance = (ServerInstance)e.Node.Tag;
  39:             this.uxOK.Enabled = ( this.selectedInstance != null );
  40:         }
  41:
  42:         private void uxLoadNetworkServers_DoWork ( object sender, DoWorkEventArgs e )
  43:         {
  44:             e.Result = ServerInstance.GetNetworkServerList ();
  45:             Console.WriteLine ( "Exiting" );
  46:         }
  47:
  48:         private void uxLoadNetworkServers_RunWorkerCompleted ( object sender, RunWorkerCompletedEventArgs e )
  49:         {
  50:             Console.WriteLine ( "Fired!" );
  51:             if ( e.Error != null )
  52:             {
  53:                 MessageBox.Show ( e.Error.Message );
  54:             }
  55:             else
  56:             {
  57:                 this.uxNetworkServers.ImageList = this.uxImages;
  58:                 this.uxNetworkServers.Nodes[0].ImageIndex = 0;
  59:                 this.uxNetworkServers.Nodes[0].SelectedImageIndex = 0;
  60:                 this.uxNetworkServers.Nodes[0].Text = "Database Engine";
  61:                 this.FillTreeView ( this.uxNetworkServers, (List<ServerInstance>)e.Result );
  62:                 this.uxNetworkServers.Enabled = true;
  63:             }
  64:         }
  65:
  66:         private void FillTreeView ( TreeView treeView, List<ServerInstance> servers )
  67:         {
  68:             foreach ( ServerInstance server in servers )
  69:             {
  70:                 TreeNode newNode = new TreeNode ( server, 1, 1 );
  71:                 newNode.Tag = server;
  72:                 treeView.Nodes[0].Nodes.Add ( newNode );
  73:             }
  74:         }
  75:
  76:         private void BrowseForServerDialog_FormClosing ( object sender, FormClosingEventArgs e )
  77:         {
  78:             if ( this.uxLoadNetworkServers.IsBusy )
  79:                 this.uxLoadNetworkServers.CancelAsync ();
  80:         }
  81:     }
  82: }

Again, nothing special.  The real magic happens in the ServerInstance class:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Data;
   6: using System.Data.Sql;
   7: using System.Net;
   8: using Microsoft.Win32;
   9:
  10: namespace FireAnt.Controls.Database
  11: {
  12:     [Serializable ()]
  13:     internal class ServerInstance : IComparable, IComparable<ServerInstance>, IComparable<string>
  14:     {
  15:         private static readonly string LocalServer = Dns.GetHostName ().ToUpper ();
  16:         private const string SqlServerRegistryKey = "SOFTWARE\\MICROSOFT\\MICROSOFT SQL SERVER";
  17:         private const string ServerInstanceRegistryKey = "SOFTWARE\\MICROSOFT\\MICROSOFT SQL SERVER\\INSTANCE NAMES\\SQL";
  18:
  19:         #region Constructors
  20:         public ServerInstance () : this ( string.Empty ) { }
  21:         public ServerInstance ( string server ) : this ( server, string.Empty ) { }
  22:         public ServerInstance ( string server, string instance ) : this ( server, instance, "0.0" ) { }
  23:         public ServerInstance ( string server, string instance, string version ) : this ( server, instance, new Version ( version ) ) { }
  24:         public ServerInstance ( string server, string instance, Version version )
  25:         {
  26:             this.Server = server;
  27:             this.Instance = instance;
  28:             this.Version = version;
  29:         }
  30:         #endregion
  31:
  32:         #region Properties
  33:         public string Server { get; private set; }
  34:         public string Instance { get; private set; }
  35:         public Version Version { get; private set; }
  36:
  37:         public string FullName
  38:         {
  39:             get
  40:             {
  41:                 return this.Server +
  42:                     ( string.IsNullOrEmpty ( this.Instance ) ? string.Empty : "\\" + this.Instance );
  43:             }
  44:         }
  45:         #endregion
  46:
  47:         #region Overrides
  48:         public override string ToString ()
  49:         {
  50:             StringBuilder displayString = new StringBuilder ( this.FullName );
  51:
  52:             // add the version if it is a valid one
  53:             if ( this.Version.Major != 0 )
  54:             {
  55:                 displayString.AppendFormat ( "    ({0}.{1})", this.Version.Major, this.Version.Minor );
  56:             }
  57:
  58:             return displayString.ToString ();
  59:         }
  60:
  61:         public override bool Equals ( object obj )
  62:         {
  63:             if ( obj == null || !( obj is ServerInstance ) )
  64:                 return false;
  65:             return ( this.ToString () == obj.ToString () );
  66:         }
  67:
  68:         public override int GetHashCode ()
  69:         {
  70:             return this.ToString ().GetHashCode ();
  71:         }
  72:         #endregion
  73:
  74:         #region Operators
  75:         public static implicit operator string ( ServerInstance instance )
  76:         {
  77:             return instance.ToString ();
  78:         }
  79:         #endregion
  80:
  81:         public static List<ServerInstance> GetLocalServerList ()
  82:         {
  83:             List<ServerInstance> localServers = new List<ServerInstance> ();
  84:
  85:             RegistryKey instancesKey = Registry.LocalMachine.OpenSubKey ( ServerInstanceRegistryKey );
  86:             if ( instancesKey != null )
  87:             {
  88:                 using ( instancesKey )
  89:                 {
  90:                     foreach ( string instanceName in instancesKey.GetValueNames () )
  91:                     {
  92:                         string instanceSetupKeyName = string.Format ( "{0}\\{1}\\Setup", SqlServerRegistryKey, (string)instancesKey.GetValue ( instanceName ) );
  93:                         RegistryKey instanceSetupKey = Registry.LocalMachine.OpenSubKey ( instanceSetupKeyName );
  94:                         if ( instanceSetupKey != null )
  95:                         {
  96:                             using ( instanceSetupKey )
  97:                             {
  98:                                 string version = (string)instanceSetupKey.GetValue ( "Version" );
  99:                                 string edition = (string)instanceSetupKey.GetValue ( "Edition" );
 100:
 101:                                 localServers.Add ( new ServerInstance ( LocalServer, ( edition == "Express Edition" ) ? instanceName : string.Empty, version ) );
 102:                             }
 103:                         }
 104:                     }
 105:                 }
 106:             }
 107:
 108:             return localServers;
 109:         }
 110:
 111:         public static List<ServerInstance> GetNetworkServerList ()
 112:         {
 113:             List<ServerInstance> networkServers = new List<ServerInstance> ();
 114:
 115:             using ( DataTable dataSources = SqlDataSourceEnumerator.Instance.GetDataSources () )
 116:             {
 117:                 foreach ( DataRow dataSource in dataSources.Rows )
 118:                 {
 119:                     networkServers.Add ( new ServerInstance (
 120:                         (string)dataSource["ServerName"],
 121:                         Convert.IsDBNull ( dataSource["InstanceName"] ) ? string.Empty : (string)dataSource["InstanceName"],
 122:                         Convert.IsDBNull ( dataSource["Version"] ) ? "0.0" : (string)dataSource["Version"] ) );
 123:                 }
 124:             }
 125:
 126:             return networkServers;
 127:         }
 128:
 129:
 130:         #region IComparable Members
 131:
 132:         public int CompareTo ( object obj )
 133:         {
 134:             if ( obj == null || !( obj is ServerInstance ) )
 135:                 return -1;
 136:             else
 137:                 return this.ToString ().CompareTo ( obj.ToString () );
 138:
 139:         }
 140:
 141:         #endregion
 142:
 143:         #region IComparable<ServerInstance> Members
 144:
 145:         public int CompareTo ( ServerInstance other )
 146:         {
 147:             return this.ToString ().CompareTo ( other.ToString () );
 148:         }
 149:
 150:         #endregion
 151:
 152:         #region IComparable<string> Members
 153:
 154:         public int CompareTo ( string other )
 155:         {
 156:             return this.ToString ().CompareTo ( other );
 157:         }
 158:
 159:         #endregion
 160:     }
 161: }

The two methods of interest are the two static methods, ServerInstance.GetLocalServerList and ServerInstance.GetNetworkServerList.  The first method – GetLocalServerList – looks in the Windows registry to locate SQL Server instances installed on the local machine, and attempts to differentiate between Express and other editions of Sql Server.  The second method – GetNetworkServerList – uses the SqlDataSourceEnumerator found in System.Data.Sql to enumerate the network servers.  At one time, I had written the code to use the Sql Server Management Objects (SMO) – but, I found that it was way slower.  I also tried the trick of using ODBC – but that didn’t really give me the information I wanted.  So, I settled on just using the SqlDataSourceEnumerator.  Still slower then I would like, but not nearly as slow as using SMO.

December 11, 2008

Creating a Managed Wrapper for a Lib File

Filed under: General .NET — Tom Shelton @ 12:51 pm

So, you have a lib file and it’s corresponding header file, and you need to use it from your managed application.  Now what?  Well, the simplest answer is to create a C++/CLR wrapper DLL to expose the functionality you need.  Here’s a step-by-step walk through of the process.

 

1.

The first step in this walk-through is to create lib file that we want to expose in our managed code.  To do this, create a blank Visual Studio Solution and give it a name.

  001
   

2.

Once we have a solution, lets add a C++ static library project to the solution.  To do this, go to Solution Explorer and right click on the solution.  On the context menu, select “Add -> New Project”.   This should bring up the “Add New Project” dialog.  Add a new C++ Win32 project.  Give it a name, and select OK.

  002
   

3.

When the “Win32 Application Wizard” comes up, select the “Next” button.  Under Application Type, select “Static Library”.  Also, for this example I turned off “Precompiled header”.  And select “Finish”.

  003
   

4.

Go to the Solution Explorer again, and right click on your static library project, and select “Add -> Class…”.  This will bring up the “Add Class” dialog, select “Add”.

  005
   

5.

Fill in the information in the “Generic C++ Class Wizard”.  Select “Finish”.

  006
   

6.

Modify the resulting AddClass.h file to look like this:

 
   1: #pragma once
   2:  
   3: namespace AddTwoNumbersLib
   4: {
   5:     class AddClass
   6:     {
   7:     public:
   8:         static double Add (double x, double y);
   9:     };
  10: }
   

7.

Let’s provide the implementation in AddClass.cpp:

 
   1: #include "AddClass.h"
   2:  
   3: namespace AddTwoNumbersLib
   4: {
   5:     double AddClass::Add(double x, double y)
   6:     {
   7:         return x + y;
   8:     }
   9: }
 

At this point, you should be able to successfully compile our example lib file.

   

8.

OK – we are now setup for the rest of this example.  We have a lib file and a header file that we will expose to our managed application.  To do this, we will create a new CLR class library, using C++/CLI.  So, go back to the Solution Explorer and right click on the solution.  Select “Add -> New Project…”.   In the Project Types treeview, select “Visual C++ -> CLR”.  In the Templates pane, select “Class Library”, and give it a name and select OK.

  007
   

9.

Now we need to set a couple of project properties to be able to use the lib file.  So, in Solution Explorer, right click on the ClrWrapper project and select Properties.  In the “ClrWrapper Property Pages”,  select “All Configurations” in the Configuration drop down.  Then, select “Configuration Properties -> C/C++ -> General”.  Modify the “Additional Include Directories” property, to point to the directory containing the header file for the .lib.

  008
 

Selecting the browse button will bring up the “Additional Include Directories” dialog.

  009
 

Clicking in the list at the top, will again give you a browse button that will bring up a folder browser dialog.  Use the folder browser to point to the directory where your lib files header is in.

  010
 

Select OK, then Apply in the “ClrWrapper Property Pages”.

   

10.

Now, we need to tell the linker where the lib file is.  We could do this by adding a project reference to the AddTwoNumbersLib project – but, since we are trying to simulate us only having the lib, we’ll do it the hard way!

With the “ClrWrapper Property Pages” dialog still open, “Configuration Properties -> Linker -> Input”.

  011
 

Select, “Additional Dependencies” and then the browse button that appears.  This will bring up the “Additional Dependencies” dialog.

  012
 

In the list at the top, insert the full path to the lib file (make sure it’s in quotes).

  013
 

Select OK.  And OK again on the “ClrWrapper Property Pages”.

   

11.

Now, lets create our wrapper.  Modify the following files

 

stdafx.h

 
   1: // stdafx.h : include file for standard system include files,
   2: // or project specific include files that are used frequently,
   3: // but are changed infrequently
   4:  
   5: #pragma once
   6:  
   7: #include "AddClass.h"
 

ClrWrapper.h

 
   1: // ClrWrapper.h
   2:  
   3: #pragma once
   4:  
   5: using namespace System;
   6:  
   7: namespace ClrWrapper {
   8:  
   9:     public ref class AddClass
  10:     {
  11:     public:
  12:         double Add (double x, double y);
  13:     };
  14: }
 

ClrWrapper.cpp

 
   1: // This is the main DLL file.
   2:  
   3: #include "stdafx.h"
   4:  
   5: #include "ClrWrapper.h"
   6: using namespace ClrWrapper;
   7:  
   8: double AddClass::Add(double x, double y)
   9: {
  10:     return AddTwoNumbersLib::AddClass::Add (x, y);
  11: }
 

With that done, you should be able to compile the solution.

   

12.

Now, create a managed project in the language of your choice – C# or VB, and add a reference to your the ClrWrapper.dll.  Once done, you should be able to write, compile, and run code like the following:

 

C#

 
   1: using System;
   2: using ClrWrapper;
   3:  
   4: namespace CSharpClient
   5: {
   6:     class Program
   7:     {
   8:         static void Main ( string[] args )
   9:         {
  10:             AddClass addClass = new AddClass ();
  11:  
  12:             Console.WriteLine ( addClass.Add ( 1, 2 ) );
  13:             Console.WriteLine ( addClass.Add ( 2, 2 ) );
  14:             Console.WriteLine ( addClass.Add ( 649, 1 ) );
  15:         }
  16:     }
  17: }
 

VB

 
   1: Option Explicit On
   2: Option Strict On
   3:  
   4: Imports System
   5: Imports ClrWrapper
   6:  
   7: Module Program
   8:  
   9:     Sub Main()
  10:         Dim a As New AddClass()
  11:  
  12:         Console.WriteLine(a.Add(1, 2))
  13:         Console.WriteLine(a.Add(2, 2))
  14:         Console.WriteLine(a.Add(649, 1))
  15:     End Sub
  16:  
  17: End Module

Hopefully, someone will find this useful.

December 2, 2008

A Simple Method for Evaluating Mathematical Expressions at Runtime

Filed under: General .NET — Tom Shelton @ 11:31 pm

Over the years, on the forums I frequent, the question has come up from time-to-time on how to dynamically evaluate expressions built at runtime.  Often, the suggestions range from dynamic code compilation to creating expression evaluators.  While these suggestions are not necessarily bad – they do have some drawbacks.

The main problem with dynamic code compilation, is that it is slow.  This may not be an issue if you are only going to do one or two calculations – but, if you need to perform your calculations hundreds or thousands of times, then the performance hit can become significant.  For example,  on the MSDN Visual C# forum, I was recently involved in a thread where the original poster was using dynamic compilation to evaluate expressions thousands of times and was looking for ways to speed up the process.  I suggested he try the method I am about to present here, and his processing time dropped from the approximately 7 hours to 49 seconds.  Not bad.

The other method, creating an expression evaluator, suffers from complexity.  It isn’t an easy task.  Of course, there are enough examples and pre-built libraries to perform this task that no one should have to write such a beast anymore (except maybe as an exercise).  But, why hunt down a third party library, when Microsoft has given us a built in expression evaluator?    Well, sort of. 

The expression evaluation mechanism that I am referring to is the JScript Eval method.  It seems that many do not realize that JScript is an official .NET language.  I suspect that is because the IDE has no built in support for JScript.NET.   Here is a method for creating a simple wrapper library to expose the JScript Eval method to your C# or VB program.

 

1.

Since there is no IDE support for the JScript.NET language, create and save the Evaluator.js file in the external text editor of your choice. 

 

image01

   

2.

Use the “Visual Studio 2008 Command Prompt” (Start -> Programs -> Microsoft Visual Studio 2008 -> Visual Studio Tools) to navigate to the directory that contains the Evaluator.js file.  Once there, compile the source, like this:

  image02
 

Ok, so I didn’t use the VS command prompt :)  I used the Windows PowerShell console.  I translated the bat file that the VS command prompt uses and put it in my $PROFILE.

   

3.

Now, create your Visual Basic or C# project.  Right click on your project in the Solution Explorer, and select “Add Reference…”.  On the .NET tab select Microsoft.JScript:

  image03
   

4.

Again, Right click on your project in the Solution Explorer, and select “Add Reference…”.   Select the “Browse” tab, and find and select the Evaluator.dll that we just compiled:

  image04
   

5.

Let’s write some code to use the dll.

C#
using System;
 
namespace CSharpExample
{
    class Program
    {
        static void Main ( string[] args )
        {
            string expression = string.Format ( "(Math.pow({0}, {1}) * {2}) / {3}", 50, 2, 3.5, 16.25 );
            Console.WriteLine ( Calculate ( expression ) );
        }
 
        static decimal Calculate ( string expression )
        {
            return Convert.ToDecimal ( new Evaluator ().Evaluate ( expression ) );
        }
    }
}
VB
Option Strict On
Option Explicit On
 
Module VBExample
 
    Sub Main()
        Dim expression As String = String.Format("(Math.pow({0}, {1}) * {2}) / {3}", 50, 2, 3.5, 16.25)
        Console.WriteLine(Calculate(expression))
    End Sub
 
    Function Calculate(ByVal expression As String) As Decimal
        Return Convert.ToDecimal(New Evaluator().Evaluate(expression))
    End Function
End Module

This example, is pretty simplistic – for example, the Calculate methods do not do any sort of checking to make sure that the expression passed is a valid mathematical expression.  I used the System.String.Format method to illustrate one way of getting values into an expression to be evaluated.  It should be noted that this method will not only evaluate mathematical expressions, but arbitrary JScript code.  As long as you are using a version of .NET >= 1.1, then this shouldn’t be much of an issue because unless you pass the string “unsafe” to the optional second parameter of the JScript Eval method, then the code will run in a limited security context.   If you decide to ever use the Eval method with the “unsafe” option, then you will want to make sure that all of your strings are obtained from a trusted source.

November 1, 2008

Calling Managed Code from a DLL Created in Visual C++ 2008

Filed under: General .NET — Tom Shelton @ 9:57 pm

I got involved in a discussion the other day over on the MSDN Visual C# General forum, in which a poster had a situation where he needed to create a dll in C++ to extend a 3rd party application – but, he wanted to reuse some of his C# code from that dll.  So, I am putting this post together to discuss the steps I took to get this working in Visual C++ 2008.  The goal of this article is to provide a step-by-step walk through.  If you follow all the steps (and I am communicating coherently) – then you should have a working code sample at the end of this article.

Since in real life, I’m primarily a C# developer, the order I do things in might be a little different then say someone who is primarily a VB.NET developer or a C++ developer.  Don’t let that throw you,  if things are in a different order,  just search around until you find the option.

 

1.

The first step is to create a new Visual C++ solution.  Click on “File -> New -> Project…”.  This brings up the “New Project” dialog.  In the “Project Types” pane, select “Other Languages -> Visual C++ -> Win32″.  Then from the “Templates” pane on the right, select “Win32 Console Application”.  Give the project a name (I called mine CallCSharpCodeSample) and select the “OK” button.

 

step1

 

When the “Application Settings Wizard” dialog appears, select the “Finish” button. The Console project just created will become the host application to test out the dll.

2.

Add a C# Class Library project.  This library will represent the existing .NET assembly that needs to exposed via the C++ dll.  Go to Solution Explorer, and right click on the solution and select “Add -> New Project…”.  In the “Project types” pane of the “Add New Project” dialog,  select “Visual C# -> Windows”.  In the “Templates” pane select “Class Library”.  Give the library a name, and then select the “OK” button.

 

step4

3.

Rename the default Class1.cs to AddClass.cs and allow the IDE to rename your class.  Insert a method to add two integers.  Your code should look something like this:

 
   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5:  
   6: namespace CustomCSharpLib
   7: {
   8:     public class AddClass
   9:     {
  10:         public int AddTwoNumbers ( int x, int y )
  11:         {
  12:             return x + y;
  13:         }
  14:     }
  15: }

4.

Setup is now complete and you’re ready to create the dll.  Right click on the solution in Solution Explorer, select “Add -> New Project…”.  Add a new C++ Win32 project from the “Add New Project” dialog.  And give it a name and select the “OK” button.

  step7

5.

You will again be presented with the “Application Settings Wizard”.  Select the “Next” button and set the project options as shown below.  Select the “Finish” button.

  step8b

6.

When the MyDll project is created, Visual Studio adds some unneeded code to “MyDll.h” and “MyDll.cpp”.  Edit these files to look like the code below.

MyDll.h:

 
   1: // The following ifdef block is the standard way of creating macros which make exporting 
   2: // from a DLL simpler. All files within this DLL are compiled with the MYDLL_EXPORTS
   3: // symbol defined on the command line. this symbol should not be defined on any project
   4: // that uses this DLL. This way any other project whose source files include this file see 
   5: // MYDLL_API functions as being imported from a DLL, whereas this DLL sees symbols
   6: // defined with this macro as being exported.
   7: #ifdef MYDLL_EXPORTS
   8: #define MYDLL_API __declspec(dllexport)
   9: #else
  10: #define MYDLL_API __declspec(dllimport)
  11: #endif
  12:  
  13: MYDLL_API int AddTwoNumbers(const int, const int);
 

MyDll.cpp:

 
   1: // MyDll.cpp : Defines the exported functions for the DLL application.
   2: //
   3:  
   4: #include "stdafx.h"
   5: #include "MyDll.h"
   6:  
   7: using namespace CustomCSharpLib;
   8:  
   9: MYDLL_API int AddTwoNumbers(const int x, const int y)
  10: {
  11:     return x + y;
  12: }
 

At this point, everything should compile just fine.  I have purposely not changed any project settings yet to enable managed code – but don’t worry, we’ll get to that!  Let’s make sure we can actually use and call this dll from the test program first.

7.

Change the default calling convention for the MyDll project to __stdcall.  You do this by right clicking on the project in the Solution Explorer and selecting “Properties…”.  Change the “Configuration” option to “All Configurations” and then select “C/C++ -> Advanced”.  Change the “Calling Convention” entry in the property page to “__stdcall (/Gz)” and select OK.

  step10
 

I’m not sure this step is actually necessary, but __stdcall is the same calling convention used by the Windows API and will make your dll easily callable from languages such as Visual Basic 6 that can’t handle the default __cdecl calling convention.

8.

Add a module definition file to the MyDll project – this will allow us to override the default name mangling done by the compiler, and make the dll more easily callable.  Right click on the MyDll project in the Solution Explorer and select “Add -> New Item…”.

9.

In the “Templates” pane of the “Add New Item” dialog, select “Module-Definition File (.def)”.  Give it a name.  Select the “Add” button.

  step12

10.

Edit your module definition file so that it looks like this (it will have a .def extension):

 
   1: LIBRARY    "MyDll"
   2:  
   3: EXPORTS
   4:     AddTwoNumbers @1

11.

Edit your CallCSharpCodeSample.cpp file to look like this:

 
   1: // CallCSharpCodeSample.cpp : Defines the entry point for the console application.
   2: //
   3:  
   4: #include "stdafx.h"
   5: #include <iostream>
   6: #include <windows.h>
   7:  
   8: using namespace std;
   9:  
  10: // our function pointer definition
  11: typedef int (WINAPI* LPFN_ADDFUNC)(const int, const int);
  12:  
  13: int _tmain(int argc, _TCHAR* argv[])
  14: {
  15:     HMODULE hMod = LoadLibrary(_T("MyDll.dll"));
  16:  
  17:     if (hMod != NULL)
  18:     {
  19:         LPFN_ADDFUNC addFunc = (LPFN_ADDFUNC)GetProcAddress(hMod, "AddTwoNumbers");
  20:  
  21:         if (addFunc != NULL)
  22:         {
  23:             cout << addFunc(1, 2) << endl;
  24:             cout << addFunc(5, 6) << endl;
  25:             cout << addFunc(100, 10024) << endl;
  26:         }
  27:         else
  28:         {
  29:             cout << "Failed to load function AddTwoNumbers" << endl;
  30:         }
  31:     }
  32:     else
  33:     {
  34:         cout << "Failed to load module MyDll.dll" << endl;
  35:     }
  36:  
  37:     return 0;
  38: }
  39:  
 

At this point, assuming you’ve done everything correctly, you should be able to compile and run the application:

  step14

12.

Now that the dll and it’s test harness are in place, let’s return our attention to the MyDll project.  At this step, we are going to enable CLR support.  Right click on the MyDll project in Solution Explorer and select “Properties…”.  Set the “Configuration” option to “All Configurations”.  Select “Configuration Properties -> General” in the menu tree, and then change the “Common Language Runtime support” option in the properties pane to “Common Language Runtime Support (/clr)” and select the “OK” button.

  step15

13.

Right click on your MyDll project in the Solution Explorer and select “References…”.  When the “Property Pages” dialog appears, select the “Add New Reference…” button.

  step18a
 

In the “Add Reference” dialog, select the “Projects” tab.  Select the CustomCSharpLib project from the list.  Select OK.

  step18b
 

This will add the reference to the C# class library to your MyDll project.  Select OK on the “Property Pages” dialog.

  step18c

14.

Edit MyDll.cpp to look like this:

 
   1: // MyDll.cpp : Defines the exported functions for the DLL application.
   2: //
   3:  
   4: #include "stdafx.h"
   5: #include "MyDll.h"
   6:  
   7: using namespace CustomCSharpLib;
   8:  
   9: MYDLL_API int AddTwoNumbers(const int x, const int y)
  10: {
  11:     AddClass addClass;
  12:     return addClass.AddTwoNumbers(x, y);
  13: }
 

Then compile and run your project.  If all goes well, this is what you should see:

  step14

One thing I did learn from this, is if you are going to include any .NET related header files (such as vcclr.h) – don’t do it in “stdafx.h”.  Otherwise, this file will be included in dllmain.cpp, and you will suddenly start getting an error about needing to add /clr switch to the command line.  The reason is that DllMain can’t be a managed function – so the IDE helpfully turns off /clr for dllmain.cpp.  There is a work around for this, you can turn on the /crl option for this file in it’s properties, but then you need to surround DllMain with some preprocessor directives to prevent it from being compiled as managed code.  It seems easier to just include any of the .NET related headers in your MyDll.h and be done with it.

Well, I hope this helps someone.  If not, it was definitely a good learning experience for me.

Older Posts »

Powered by WordPress