Category Archives: Code Garage

Gaining Access to a Running Instance of Windows Media Player In VB.net

11
Filed under Code Garage, VB Feng Shui, WindowsMediaPlayer

Over the past few weeks, I’ve been working on a little screensaver side project to run in an old radio cabinet that I’ve converted into a touchscreen jukebox. Details of that project will be coming, but for now, I thought I’d share some interesting software bits I’ve discovered.

The screensaver that I’ve working on is intended to:

1) Recognize a Media Player that’s running on the computer when the screensaver loads up.

2) Connect to and monitor that media player for any changes in what’s it playing. Particularly, I want to know Artist name, album name and track name.

3) Hit a few search engines for some images related to what’s playing and display them in a nice “Ken Burns Pan and Zoom” style animation.

Well, long story short, WPF made short work of the image animations, and connecting to two of my favorite media players, J River Media Center and Album Player, was pretty trivial.

But, within just a few days of posting the first version, someone asked if it worked, or could work, with Windows Media Player (and, as it turns out, Windows Media Center, which is just a nicer shell over WMP).

Why Not?

My first thought was, sure! All I have to do it be able to see what’s currently playing and when it changes. Shouldn’t be too tough, right?

Well, after quite of bit of digging, it turns out that Windows Media Player (WMP), is far more gracious about hosting “plugins” that it is about being remotely attached to. There are several plugins available for WMP that write out info on the current playing media to an XML file, or the registry, or a web service, or whatever. But that requires "my application’s user” to install some other application to make things work. Not cool. At least, not for me.

Plan two. Most MS Office apps register themselves with the Running Object Table (The ROT). Other programs can query the ROT and retrieve objects from it that they’re interested in connected to. You often see the VB GetObject() function used for this purpose.

But WMP doesn’t register itself with the ROT, so that’s not an option.

On to Plan C.

WMP Remoting

However, as luck would have it, MS did do something about this type of situation. They call it “Media Player Remoting”. However, it’s just about the least documented concept I’ve come across yet. There’s just very little info about exactly how to set up this “remoting” or what it’s capable of.

Eventually,though, I did come across mention of a demo project written by Jonathan Dibble, of Microsoft no less, that illustrates the technique in C#. There’s a thread here that contains links to the original code, though that page appears to be in Japanese.

Looking further, I found several variations of Dibble’s project, some vastly more involved and complex than others.

I grabbed the simpler version and started hacking!

The Conversion

Converting Mr. Dibble’s code was fairly straightforward. He did a pretty fair job in commenting it and breaking things down nicely. As usual, one of my favorite Web Resources, DeveloperFusion’s CodeConverter, got a workout, and did a fine job on most of the conversion gruntwork.

But when the dust cleared, it didn’t work.

After a lot of stepping with the debugger, it turns out that Jonathan’s handling of  IOleClientSite.GetContainer isn’t quite right. His original code threw a “Not implemented” exception that would cause a crash for me every single time.

The function itself isn’t particularly useful for what I needed to do, and after reading up on the documentation, I felt certain that there’s really wasn’t anything that “needed” to be done in that function. But, it did get called by WMP, and something other than throwing an exception had to be done there.

Then, I realized that a number of other OLE-centric functions that Jonathan had implemented had a return value of HRESULT, and simply returned a E_NOTIMPL value.

So, I changed up the definition of GetContainer and had it returning an E_NOTIMPL, and presto! It works!

Since Jonathan’s demo project appears to be at least 4 years old, I’m not sure whether I may have indeed worked that way at one point, or was a bug, or, quite possibly, was something I didn’t get quite right in the conversion in the first place. Regardless, this version works, so bob’s your uncle.

How to Use It

For anyone not interested in the details, I’ll dive right in to how you actually use this technique.

First off, you’ll need to add a reference to WMP.DLL. This file should be in your Windows\system32 folder if a Windows Media Player is installed. Once added, you’ll have a WMPLib reference in your References tab:

image

Next, copy the WMPRemote.vb file into your own project.

Finally, though this isn’t strictly necessary, you may want to alter your project’s AssemblyInfo.vb file and set the CLSAttribute to false.

....
<Assembly: AssemblyCopyright("blah")> 
<Assembly: AssemblyTrademark("blah")> 
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'THE BELOW LINE YOU MIGHT ADD OR CHANGE TO "FALSE"
<Assembly: CLSCompliant(False)> 

'The following GUID is for the ID of the typelib if this project is exposed to COM
<Assembly: Guid("12333333-33454-1233-1234-123451234512")> 
.....

The main class to work with is WMPRemote. It has two static properties; IsWindowMediaPlayerLoaded, and ActivePlayer.

These are static properties, so you access them using the WMPRemote class, like so:

If WMPRemote.IsWindowsMediaPlayerLoaded Then
   Dim Player = WMPRemote.ActivePlayer
End If

At that point, if WMP is loaded, you’ll have a reference to the full WMPlib.WindowsMediaPlayer object in the Player variable.

From there, you can do whatever you need to.

Query properties:

Debug.print Player.playState 

Attach to events:

AddHandler Player.CurrentItemChange, AddressOf CurrentItemChange
AddHandler Player.PlayStateChange, AddressOf PlayStateChange

Or whatever else is necessary.

How It Works

WMPRemote

Since this class is the main access point, I’ll start here. There are only 2 static functions defined here:

IsWindowsMediaPlayerLoaded

This function simply uses the .net Processes object to query for any processes named WMPlayer. If there are any, it returns TRUE, if not, FALSE. Obviously, it could be wrong, but that’s not terribly likely.

ActivePlayer

If things have already been initialized, this function just returns whatever it’s already retrieved for the Active WindowsMediaPlayer object.

If not, it checks if WMP appears to be loaded and, if so, creates and shows an instance of the internal frmWMPRemote form. During the load of this form, it’s immediately hidden so the user will never see it.

The only purpose of frmWMPRemote, is to host the WindowsMediaPlayer ActiveX Control. This all happens during the form Load event:

Me.Opacity = 0
Me.ShowInTaskbar = False
_InternalPlayer = New WMPRemoteAx
_InternalPlayer.Dock = System.Windows.Forms.DockStyle.Fill
Me.Controls.Add(_InternalPlayer)
Me.Hide()

Note that it actually is creating an instance of the WMPRemoteAx control, and then siting it on the form.

WMPRemoteAx

This class is based on the AxHost control, and is what allows an ActiveX COM-based control to exist on a .net WinForms form.

Once created as actually sited on a control (or WinForms Form, in this case), the AttachInterfaces method is called by the .net runtime to connect this host up with whatever COM ActiveX Control it will be hosting. I’ve told this control to host the WindowsMediaPlayer ActiveX control by setting the GUID in the constructor:

MyBase.New("6bf52a52-394a-11d3-b153-00c04f79faa6")

AttachInterfaces connects up the ActiveX control with a clientsite as necessary, but more importantly, it exposes the internal OCX instance by casting it as an instance of WindowsMediaPlayer:

Dim oleObject As IOleObject = TryCast(Me.GetOcx(), IOleObject)

Dim a = TryCast(Me, IOleClientSite)
oleObject.SetClientSite(a)

_RemotePlayer = DirectCast(Me.GetOcx(), WMPLib.WindowsMediaPlayer)

At this point, COM calls several other interfaces that have been implemented by WMPRemoteAx, including:

  • IOleServiceProvider
  • IOleClientSite

    Most of the methods on these interfaces need not actually do anything. They are required for more sophisticated integrations. However, IOleServiceProvider_QueryService does have a very specific purpose.

    Remoting

    Remember that this entire situation is made possible by something WMP calls “Remoting”. Turns out, this callback method is how our control communicates to WMP that we are, in fact, setting up a remoting situation.

    If riid = New Guid("cbb92747-741f-44fe-ab5b-f1a48f3b2a59") Then
        Dim iwmp As IWMPRemoteMediaServices = New RemoteHostInfo()
        Return Marshal.GetComInterfaceForObject(iwmp, GetType(IWMPRemoteMediaServices))
    End If

    When WMP calls this function with the given riid as above, our control has to respond by returning an object of type IWMPRemoteMediaServices (in this case implemented by the RemoteHostInfo object in the project).  That object has a few properties that WMP queries for some basic information, but really, the fact that our control (WMPRemoteAx) has responded by returning an IWMPRemoteMediaServices is the main thing that sets up the remoting connection.

    The OLE Interfaces and Enums

    The rest of WMPRemote.vb is made up of several COM Enums and interfaces that are necessary to bring all this together, but that aren’t readily defined in VB.net. None of that code should normally be altered in any way because it’s actually defining interfaces and values that have already been defined in COM, but that we need defined for use in a .net application. For instance:

    <ComImport(), ComVisible(True), Guid("00000118-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
    Public Interface IOleClientSite

    This is the start of the definition of the IOleClientSite interface.  That interface is a “well defined” interface with a specific GUID (the value of the GUID attribute), and a specific interface type. Changing our .net definition of that interface would likely break everything.

    Where’s the code?

    Right here. This particular version is in VS2010 with .net 4.0 framework, but I don’t imagine there’d be any problems converting back to VS2008, or possibly even VS2005. I suspect you will need at least .net framework 2.0 though, but I haven’t tested this.

    Also, check out a few of my other projects in the Code Garage.

    And finally, let me know what you think!

  • Exposing C-Style Entry Points in a .net Assembly (revisited)

    7
    Filed under .NET, Code Garage

    I’ve written about exposing Entry Points from a .net dll before, most recently here.

    But I recently came across another take on the subject that was so clean, I thought I’d record it and a link here, just to be complete.

    First, a little background. Occasionally, I’ve found I have need to expose standard c-style entry points from a .net dll. This usually centers around integrating with some application, typically plugins, where the hosting app attempts to use LoadLibrary on your dll and then resolve a specific entrypoint via GetProcAddress.

    I’ve done this kind of thing in a variety of ways, C or C++ wrappers, ASM, etc, but .net doesn’t provide any way to expose entry points.

    Or does it?

    In reality, neither C# or VB.net allow such functionality, but MSIL (Microsoft Intermediate Language) does. In fact, this has been around for so long, there’s actually several different approaches to implementing this functionality out on the web.

    The two I know about are:

    I had some troubles with Selvin’s version, but Mr. Giesecke’s version resolved the issue nicely at the time.

    However, after looking over the 3’rd option, I have to say I like it a little more. I should point out, though, that I’ve only used Mr. Giesecke’s approach with .net 3.5, and the other approach with .net 4.0, so keep that in mind.

    Essentially, the way these utilities work is to use ILDASM to disassembly a compiled .net assembly, they then read the resulting IL file, tweak it in a few specific ways, and finally use ILASM to reassemble the project.

    One important note here: ILASM.exe actually comes with the .net runtime and as such, it’s already on your computer if you have the .net runtime installed.

    On the other hand, ILDASM comes with the .net framework SDK, which is NOT part of the framework runtime. You’ll need to download and install the SDK in order to have ILDASM available. You can get the 2.0 SDK here.

    On to the Code

    Mr. Giesecke’s utility is well documented and I won’t reproduce that here.

    The source code for the other utility I mentioned was posted by the author in a forum thread. It’s C#, and well, this is VBFengShui, plus I wanted to ferret through it and understand what was going on a little more than I had in the past, so converting it to VB seemed like a good idea. Plus I cleaned up a few minor nits here and there to boot.

    The final program is listed below. It’s fully contained in a single class. It’s long, but not that long.

    Imports System.Text
    Imports System.IO
    Imports System.Reflection
    Imports Microsoft.Win32
    Imports System.ComponentModel
    Imports System.Runtime.InteropServices
    Imports System.Runtime.CompilerServices
    
    Namespace DllExport
        '
        '   Export native 64bit method from .NET assembly
        '   =============================================
        '   Adapted from code found here
        '   http://social.msdn.microsoft.com/Forums/en-US/clr/thread/8648ff5e-c599-42e4-b873-6b91205a5c93/
        '
        '   More info
        '   http://msdn.microsoft.com/en-us/library/ww9a897z.aspx
        '   http://stackoverflow.com/questions/2378730/register-a-c-vb-net-com-dll-programatically
        '
        '
        '   ==================================
        '   Comments from the original project
        '   ==================================
        '   It is well known fact that .NET assembly could be tweaked to export native method,
        '   similar way how normal DLLs do it. There is good description and tool from Selvin
        '   for 32bit native function.
        '
        '   My problem was how to do it for 64bits. Here you go.
        '
        '   1) you ILDAsm your assembly into il code.
        '   2) Edit the IL and change header to look like this:
        '
        '   For 32bit:
        '      .corflags 0x00000002
        '      .vtfixup [1] int32 fromunmanaged at VT_01
        '      .data VT_01 = int32[1]
        '
        '   For 64bit
        '      .corflags 0x00000008
        '      .vtfixup [1] int64 fromunmanaged at VT_01
        '      .data VT_01 = int64[1]
        '
        '   3) Header of your exported method would look similar to this. This is same for 32bit version.
        '      .vtentry 1 : 1
        '      .export [1] as Java_net_sf_jni4net_Bridge_initDotNet
        '
        '   4) You ILAsm the file back into DLL. For x64 you use /x64 flag.
        '
        '   5) Update: It looks like none of the .vtfixup, .data or .vtentry changes are required any more to make this work.
        '      This simplifies the parser quite a lot. We only need to change .corflags and modify the method signature
        '
        '   Usage requires a build step which includes this
        '      if "$(OutDir)"=="bin\Debug\" (set EXPORTARGS=/debug /name32:" x86" /name64:" x64") ELSE (set EXPORTARGS=/name32:" x86" /name64:" x64")
        '      "$(SolutionDir)Utilities\DllExport.exe" %EXPORTARGS% /input:"$(TargetPath)"
        '
        '   You can, of course, choose not to build the x86 or x64 versions by leaving out the
        '   applicable /name: tag
        '
    
        ''' <summary>
        ''' Class to export attributed functions as standard cdecl functions
        ''' </summary>
        Class DLLExport
    #Region "Enums"
            Private Enum Platform
                x86
                x64
            End Enum
    #End Region
    
    #Region "Fields"
            Private rInputFile As String
            Private rOutputFile As String
            Private rDebugOn As Boolean
            Private rVerboseOn As Boolean
            Private rLines As New List(Of String)()
            Private rExportIdx As Integer
            Private rX86Suffix As String
            Private rX64Suffix As String
            Private rExportX86 As Boolean
            Private rExportX64 As Boolean
    #End Region
    
    #Region " EntryPoint"
            Public Shared Sub Main(args As String())
                Dim dllexport = New DLLExport
    
    #If DEBUG Then
                '---- these are a few Debugging command lines
                'string[] testargs = {"", "/debug", "/name32:\"-x86\"", "/name64:\"-x64\"", "/input:\"..\\..\\..\\DLLRegister\\bin\\release\\DllRegisterRaw.dll\"", "/output:\"..\\..\\..\\DLLRegister\\bin\\release\\DllRegister.dll\"" };
                'Dim testargs As String() = {"", "/debug", "/name32:""-x86""", "/input:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxS.dll"""}
                args = "|/debug|/input:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxSRaw.dll""|/output:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxS.dll""".Split("|")
    #End If
                Dim cdir As String = ""
                Dim r As Integer = 1
                Try
                    cdir = System.IO.Directory.GetCurrentDirectory()
                    r = dllexport.Execute(args)
                Catch ex As Exception
                    Console.WriteLine("")
                    Console.WriteLine(String.Format("Unable to process file: \r\n{0}", ex.ToString))
                Finally
                    System.IO.Directory.SetCurrentDirectory(cdir)
                End Try
    
                '---- return an application exit code
                Environment.ExitCode = r
            End Sub
    #End Region
    
    #Region "Initialization"
    
            ''' <summary>
            ''' Constructor
            ''' </summary>
            Public Sub New()
                'Nothing special
            End Sub
    #End Region
    
    #Region "Properties"
    
            ''' <summary>
            ''' Get just the file name without extension
            ''' </summary>
            Private ReadOnly Property FileName() As String
                Get
                    Return Path.GetFileNameWithoutExtension(rInputFile)
                End Get
            End Property
    
            ''' <summary>
            ''' Get the folder that contains the file
            ''' </summary>
            Private ReadOnly Property FileFolder() As String
                Get
                    Return Path.GetDirectoryName(rInputFile)
                End Get
            End Property
    
            ''' <summary>
            ''' Get the path to the disassembler
            ''' </summary>
            Private ReadOnly Property DisassemblerPath() As String
                Get
                    Dim registryPath = "SOFTWARE\Microsoft\Microsoft SDKs\Windows"
                    Dim registryValue = "CurrentInstallFolder"
                    Dim key = If(Registry.LocalMachine.OpenSubKey(registryPath), Registry.CurrentUser.OpenSubKey(registryPath))
    
                    If key Is Nothing Then
                        Throw New Exception("Cannot locate ildasm.exe.")
                    End If
    
                    Dim SDKPath = TryCast(key.GetValue(registryValue), String)
    
                    If SDKPath Is Nothing Then
                        Throw New Exception("Cannot locate ildasm.exe.")
                    End If
    
                    SDKPath = Path.Combine(SDKPath, "Bin\ildasm.exe")
    
                    If Not File.Exists(SDKPath) Then
                        Throw New Exception("Cannot locate ildasm.exe.")
                    End If
    
                    Return SDKPath
                End Get
            End Property
    
            ''' <summary>
            ''' Get the path to the assembler
            ''' </summary>
            Private ReadOnly Property AssemblerPath() As String
                Get
                    Dim version = Environment.Version.Major.ToString() & "." & Environment.Version.Minor.ToString() & "." & Environment.Version.Build.ToString()
    
                    Dim ILASMPath = Environment.ExpandEnvironmentVariables("%SystemRoot%\Microsoft.NET\Framework\v" & version & "\ilasm.exe")
    
                    If Not File.Exists(ILASMPath) Then
                        Throw New Exception("Cannot locate ilasm.exe.")
                    End If
    
                    Return ILASMPath
                End Get
            End Property
    #End Region
    
    #Region "Public Methods"
    
            ''' <summary>
            ''' Run the conversion
            ''' </summary>
            ''' <returns>An integer used as the DOS return value (0-success, 1 failed)</returns>
            Public Function Execute(inargs As String()) As Integer
                Console.WriteLine("DLLExport Tool v{0}", My.Application.Info.Version.ToString)
                Console.WriteLine("Utility to create old-style dll entry points in .net assemblies")
                Console.WriteLine("")
    
                If ProcessArguments(inargs) Then
                    ' Show usage
                    Console.WriteLine("usage: DllExport.exe assembly [/Release|/Debug] [/Verbose] [/Out:new_assembly] [/name32:32bitsuffix] [/name64:64bitsuffix] ")
                    Console.WriteLine("")
                    Console.WriteLine("If neither name32 or name64 is specified, only a 32bit output will be generated.")
                    Return 1
                End If
    
                If Not File.Exists(rInputFile) Then
                    Throw New Exception("The input file does not exist: '" & rInputFile & "'")
                End If
    
                WriteInfo("DllExport Tool")
                WriteInfo(String.Format("Debug: {0}", rDebugOn))
                WriteInfo(String.Format("Input: '{0}'", rInputFile))
                WriteInfo(String.Format("Output: '{0}'", rOutputFile))
    
                Console.WriteLine("")
    
                Disassemble()
                ReadLines()
                '---- for debugging, backup the original il
                'SaveLines(@"C:\Temp\DllExport\Disassembled Original.il");
                ParseAllDllExport()
    
                '---- 32-bit
                If rExportX86 Then
                    FixCorFlags(Platform.x86)
                    '---- for debugging, back up the tweaked il
                    'SaveLines(@"C:\Temp\DllExport\Disassembled x86.il");
                    Assemble(Platform.x86)
                End If
    
                '---- 64-bit
                If rExportX64 Then
                    FixCorFlags(Platform.x64)
                    '---- for debugging, back up the tweaked il
                    'SaveLines(@"C:\Temp\DllExport\Disassembled x64.il");
                    Assemble(Platform.x64)
                End If
    
                Dim exportCount As Integer = rExportIdx - 1
                Console.WriteLine("DllExport: Exported " & exportCount & (If(exportCount = 1, " function", " functions")))
    
                Console.WriteLine()
                Return 0
            End Function
    #End Region
    
    #Region "Private, Protected Methods"
    
            ''' <summary>
            ''' Parse the arguments
            ''' </summary>
            Private Function ProcessArguments(inargs As String()) As Boolean
                rDebugOn = False
                rVerboseOn = False
                rInputFile = Nothing
                rOutputFile = Nothing
                rX86Suffix = Nothing
                rX64Suffix = Nothing
                rExportX86 = False
                rExportX64 = False
    
                '---- mainly for testing to allow swapping out command line args programmatically
                Dim args As String()
                If inargs Is Nothing Then
                    args = Environment.GetCommandLineArgs()
                Else
                    args = inargs
                End If
    
                '---- parse each command line arg
                For idx = 1 To args.Length - 1
                    Dim argLower = args(idx).ToLower()
    
                    If argLower.StartsWith("/name32:") Then
                        rExportX86 = True
                        rX86Suffix = args(idx).Substring(8).Trim("""".ToCharArray())
                    ElseIf argLower.StartsWith("/name64:") Then
                        rExportX64 = True
                        rX64Suffix = args(idx).Substring(8).Trim("""".ToCharArray())
                    ElseIf argLower = "/debug" Then
                        rDebugOn = True
                    ElseIf argLower = "/verbose" Then
                        rVerboseOn = True
                    ElseIf argLower.StartsWith("/input:") Then
                        rInputFile = args(idx).Substring(7).Trim("""".ToCharArray())
                    ElseIf argLower.StartsWith("/output:") Then
                        rOutputFile = args(idx).Substring(8).Trim("""".ToCharArray())
                    End If
                Next
    
                '---- if neither x86 or x64, then assume x86
                If Not rExportX86 AndAlso Not rExportX64 Then
                    rExportX86 = True
                End If
    
                If rInputFile = String.Empty OrElse rInputFile Is Nothing Then
                    Throw New Exception("You must provide a filename to process.")
                Else
                    If Not File.Exists(rInputFile) OrElse Me.FileFolder = String.Empty Then
                        '---- if there's no folder for inputfile, assume the current folder
                        rInputFile = Path.Combine(Directory.GetCurrentDirectory(), rInputFile)
    
                        If Not File.Exists(rInputFile) Then
                            '---- still can't find the input file, bail
                            Throw New Exception(String.Format("The input file does not exist: '{0}'", rInputFile))
                        End If
                    End If
    
                    '---- if no output specified, use the same as input
                    If String.IsNullOrEmpty(rOutputFile) Then
                        rOutputFile = rInputFile
                    End If
    
                    '---- return true on failure, false on success
                    Return String.IsNullOrEmpty(rInputFile)
                End If
            End Function
    
            ''' <summary>
            ''' Disassemble the input file
            ''' </summary>
            Private Sub Disassemble()
                rExportIdx = 1
                System.IO.Directory.SetCurrentDirectory(Me.FileFolder)
                Dim proc As New Process()
    
                ' Must specify the /caverbal switch in order to get the custom attribute
                ' values as text and not as binary blobs
                Dim arguments As String = String.Format("/nobar{1}/out:""{0}.il"" ""{0}.dll""", Me.FileName, " /linenum /caverbal ")
    
                WriteInfo("Disassemble file with arguments '" & arguments & "'")
    
                Dim info As New ProcessStartInfo(Me.DisassemblerPath, arguments)
    
                info.UseShellExecute = False
                info.CreateNoWindow = False
                info.RedirectStandardOutput = True
                proc.StartInfo = info
    
                Try
                    proc.Start()
                Catch e As Win32Exception
                    Dim handled As Boolean = False
    
                    If e.NativeErrorCode = 3 Then
                        ' try to check wow64 program files
                        Dim fn As String = info.FileName
    
                        If fn.Substring(1, 16).ToLower() = ":\program files\" Then
                            info.FileName = fn.Insert(16, " (x86)")
                            handled = True
                            proc.Start()
                        End If
                    End If
                    If Not handled Then
                        Throw (e)
                    End If
                End Try
    
                proc.WaitForExit()
    
                If proc.ExitCode <> 0 Then
                    WriteError(proc.StandardOutput.ReadToEnd())
                    Throw New Exception("Could not Disassemble: Error code '" & proc.ExitCode & "'")
                End If
            End Sub
    
            ''' <summary>
            ''' Read all the lines from the disassembled IL file
            ''' </summary>
            Private Sub ReadLines()
                rLines.Clear()
    
                If String.IsNullOrEmpty(rInputFile) Then
                    Throw New Exception("The input file could not be found")
                End If
    
                Dim ilFile As String = Me.FileName & ".il"
    
                If Not File.Exists(ilFile) Then
                    Throw New Exception("The disassembled IL file could not be found")
                End If
    
                Dim sr As StreamReader = File.OpenText(ilFile)
    
                While Not sr.EndOfStream
                    Dim line As String = sr.ReadLine()
                    rLines.Add(line)
                End While
    
                sr.Close()
                sr.Dispose()
            End Sub
    
            ''' <summary>
            ''' Save the current lines to the specified file
            ''' </summary>
            Private Sub SaveLines(fileName As String)
                Try
                    Dim folder = Path.GetDirectoryName(fileName)
    
                    If Not Directory.Exists(folder) Then
                        Directory.CreateDirectory(folder)
                    End If
    
                    Dim fileStream = File.CreateText(fileName)
    
                    For Each line As String In rLines
                        fileStream.WriteLine(line)
                    Next
    
                    fileStream.Close()
                Catch
                End Try
            End Sub
    
            ''' <summary>
            ''' Fix the Cor flags
            ''' </summary>
            Private Sub FixCorFlags(platform__1 As Platform)
                For idx As Integer = 0 To rLines.Count - 1
                    If rLines(idx).StartsWith(".corflags") Then
                        Select Case platform__1
                            Case Platform.x86
                                rLines(idx) = ".corflags 0x00000002  // 32BITREQUIRED"
                                Exit Select
    
                            Case Platform.x64
                                rLines(idx) = ".corflags 0x00000008  // 64BITREQUIRED"
                                Exit Select
                        End Select
                        Exit For
                    End If
                Next
            End Sub
    
            ''' <summary>
            ''' Parse all DllExport entries
            ''' </summary>
            Private Sub ParseAllDllExport()
                Dim dllExportIdx As Integer = FindAttributeLine(-1, -1)
    
                While dllExportIdx >= 0
                    ParseDllExport(dllExportIdx)
                    dllExportIdx = FindAttributeLine(dllExportIdx + 1, -1)
                End While
            End Sub
    
            ''' <summary>
            ''' Parse the DllExport entry
            ''' </summary>
            ''' <param name="dllExportIdx"></param>
            Private Sub ParseDllExport(dllExportIdx As Integer)
                Dim exportNameIdx As Integer = FindLineContains("string('", True, dllExportIdx, dllExportIdx + 5)
                Dim calConvIdx As Integer = FindLineContains("int32(", True, dllExportIdx, dllExportIdx + 5)
                Dim exportName As String = Nothing
                Dim startIdx As Integer = 0
                Dim endIdx As Integer = 0
    
                If calConvIdx < 0 Then
                    Throw New Exception("Could not find Calling Convention for line " & dllExportIdx.ToString())
                End If
    
                If exportNameIdx >= 0 Then
                    startIdx = rLines(exportNameIdx).IndexOf("('")
                    endIdx = rLines(exportNameIdx).IndexOf("')")
    
                    If startIdx >= 0 AndAlso endIdx >= 0 Then
                        exportName = rLines(exportNameIdx).Substring(startIdx + 2, endIdx - startIdx - 2)
                    End If
                End If
    
                startIdx = rLines(calConvIdx).IndexOf("int32(")
                endIdx = rLines(calConvIdx).IndexOf(")")
    
                If startIdx < 0 OrElse endIdx < 0 Then
                    Throw New Exception("Could not find Calling Convention for line " & dllExportIdx.ToString())
                End If
    
                Dim calConvText As String = rLines(calConvIdx).Substring(startIdx + 6, endIdx - startIdx - 6)
                Dim calConvValue As Integer = 0
    
                If Not Integer.TryParse(calConvText, calConvValue) Then
                    Throw New Exception("Could not parse Calling Convention for line " & dllExportIdx.ToString())
                End If
    
                Dim callConv As CallingConvention = CType(calConvValue, CallingConvention)
    
                Dim endDllExport As Integer = FindLineContains("}", True, calConvIdx, calConvIdx + 10)
    
                If endDllExport < 0 Then
                    Throw New Exception("Could not find end of Calling Convention for line " & dllExportIdx.ToString())
                End If
    
                ' Remove the DllExport lines
                While endDllExport >= dllExportIdx
                    rLines.RemoveAt(System.Math.Max(System.Threading.Interlocked.Decrement(endDllExport), endDllExport + 1))
                End While
    
                Dim insertIdx As Integer = FindLineStartsWith(".maxstack", True, dllExportIdx, dllExportIdx + 20)
    
                If insertIdx < 0 Then
                    Throw New Exception("Could not find '.maxstack' insert location for line " & dllExportIdx.ToString())
                End If
    
                Dim tabs As Integer = rLines(insertIdx).IndexOf(".")
    
                Dim exportText As String = TabString(tabs) & ".export [" & (System.Math.Max(System.Threading.Interlocked.Increment(rExportIdx), rExportIdx - 1)).ToString() & "]"
    
                If Not String.IsNullOrEmpty(exportName) Then
                    exportText += " as " & exportName
                End If
    
                rLines.Insert(insertIdx, exportText)
    
                Dim methodName As String = UpdateMethodCalConv(FindLineStartsWith(".method", False, insertIdx - 1, -1), callConv)
    
                If Not String.IsNullOrEmpty(methodName) Then
                    If Not String.IsNullOrEmpty(exportName) Then
                        Console.WriteLine("Exported '" & methodName & "' as '" & exportName & "'")
                    Else
                        Console.WriteLine("Exported '" & methodName & "'")
                    End If
                End If
            End Sub
    
            ''' <summary>
            ''' Update the method's calling convention
            ''' </summary>
            ''' <param name="methodIdx"></param>
            ''' <param name="callConv"></param>
            Private Function UpdateMethodCalConv(methodIdx As Integer, callConv As CallingConvention) As String
                If methodIdx < 0 OrElse FindLineStartsWith(".method", True, methodIdx, methodIdx) <> methodIdx Then
                    Throw New Exception("Invalid method index: " & methodIdx.ToString())
                End If
    
                Dim endIdx As Integer = FindLineStartsWith("{", True, methodIdx, -1)
    
                If endIdx < 0 Then
                    Throw New Exception("Could not find method open brace location for line " & methodIdx.ToString())
                End If
    
                endIdx -= 1
                Dim insertLine As Integer = -1
                Dim insertCol As Integer = -1
                Dim methodName As String = Nothing
    
                For idx As Integer = methodIdx To endIdx
                    Dim marshalIdx As Integer = rLines(idx).IndexOf("marshal(")
    
                    If marshalIdx >= 0 Then
                        ' Must be inserted before the "marshal(" entry
                        insertLine = idx
                        insertCol = marshalIdx
                        Exit For
                    Else
                        Dim openBraceIdx As Integer = rLines(idx).IndexOf("("c)
    
                        While openBraceIdx >= 0 AndAlso insertLine < 0 AndAlso insertCol < 0
                            Dim spaceIdx As Integer = rLines(idx).LastIndexOf(" "c, openBraceIdx)
    
                            If spaceIdx >= 0 Then
                                Dim findMethodName As String = rLines(idx).Substring(spaceIdx + 1, openBraceIdx - spaceIdx - 1)
    
                                ' The method name is anything but "marshal"
                                If findMethodName <> "marshal" Then
                                    insertLine = idx
                                    insertCol = spaceIdx + 1
                                    methodName = findMethodName
                                    Exit While
                                End If
    
                                openBraceIdx = rLines(idx).IndexOf("("c, openBraceIdx + 1)
                            End If
                        End While
                    End If
    
                    If methodIdx >= 0 AndAlso insertCol >= 0 Then
                        Exit For
                    End If
                Next
    
                If insertLine < 0 OrElse insertCol < 0 Then
                    Throw New Exception("Could not find method name for line " & methodIdx.ToString())
                End If
    
                Dim leftText As String = rLines(insertLine).Substring(0, insertCol)
                Dim rightText As String = rLines(insertLine).Substring(insertCol)
                Dim callConvText As String = "modopt([mscorlib]"
    
                Select Case callConv
                    Case System.Runtime.InteropServices.CallingConvention.Cdecl
                        callConvText += GetType(CallConvCdecl).FullName & ") "
                        Exit Select
    
                    Case System.Runtime.InteropServices.CallingConvention.FastCall
                        callConvText += GetType(CallConvFastcall).FullName & ") "
                        Exit Select
    
                    Case System.Runtime.InteropServices.CallingConvention.StdCall
                        callConvText += GetType(CallConvStdcall).FullName & ") "
                        Exit Select
    
                    Case System.Runtime.InteropServices.CallingConvention.ThisCall
                        callConvText += GetType(CallConvThiscall).FullName & ") "
                        Exit Select
    
                    Case System.Runtime.InteropServices.CallingConvention.Winapi
                        callConvText += GetType(CallConvStdcall).FullName & ") "
                        Exit Select
                    Case Else
    
                        Throw New Exception("Invalid calling convention specified: '" & callConv.ToString() & "'")
                End Select
    
                rLines(insertLine) = leftText & callConvText & rightText
                Return methodName
            End Function
    
            ''' <summary>
            ''' Assemble the destination file
            ''' </summary>
            Private Sub Assemble(platform__1 As Platform)
                Dim sw As StreamWriter = File.CreateText(Me.FileName & ".il")
    
                For Each line As String In rLines
                    sw.WriteLine(line)
                Next
    
                sw.Close()
                sw.Dispose()
    
                Dim resFile As String = Me.FileName & ".res"
                Dim res As String = """" & resFile & """"
    
                If File.Exists(resFile) Then
                    res = " /resource=" & res
                Else
                    res = ""
                End If
    
                Dim proc As New Process()
                Dim extension As String = Path.GetExtension(rInputFile)
                Dim outFile As String = Path.GetFileNameWithoutExtension(rOutputFile)
    
                Select Case platform__1
                    Case Platform.x86
                        If Not String.IsNullOrEmpty(rX86Suffix) Then
                            outFile += rX86Suffix
                        End If
    
                    Case Platform.x64
                        If Not String.IsNullOrEmpty(rX64Suffix) Then
                            outFile += rX64Suffix
                        End If
                End Select
    
                If extension = String.Empty Then
                    extension = ".dll"
                End If
    
                outFile += extension
    
                Dim argOptions As String = "/nologo /quiet /DLL"
                Dim argIl As String = """" & Me.FileName & ".il"""
                Dim argOut As String = "/out:""" & outFile & """"
    
                If rDebugOn Then
                    argOptions += " /debug /pdb"
                Else
                    argOptions += " /optimize"
                End If
    
                If platform__1 = Platform.x64 Then
                    argOptions += " /x64"
                End If
    
                Dim arguments As String = argOptions & " " & argIl & " " & res & " " & argOut
    
                WriteInfo(String.Format("Compiling file with arguments '{0}", arguments))
    
                Dim info As New ProcessStartInfo(Me.AssemblerPath, arguments)
                info.UseShellExecute = False
                info.CreateNoWindow = False
                info.RedirectStandardOutput = True
                proc.StartInfo = info
                proc.Start()
                proc.WaitForExit()
    
                WriteInfo(proc.StandardOutput.ReadToEnd())
    
                If proc.ExitCode <> 0 Then
                    Throw New Exception(String.Format("Could not assemble: Error code '{0}'", proc.ExitCode))
                End If
            End Sub
    
            ''' <summary>
            ''' Find the next line that starts with the specified text, ignoring leading whitespace
            ''' </summary>
            ''' <param name="findText"></param>
            ''' <param name="startIdx"></param>
            ''' <param name="endIdx"></param>
            ''' <returns></returns>
            Private Function FindLineStartsWith(findText As String, forward As Boolean, startIdx As Integer, endIdx As Integer) As Integer
                If forward Then
                    If startIdx < 0 Then
                        startIdx = 0
                    End If
    
                    If endIdx < 0 Then
                        endIdx = rLines.Count - 1
                    Else
                        endIdx = Math.Min(endIdx, rLines.Count - 1)
                    End If
    
                    For idx As Integer = startIdx To endIdx
                        If rLines(idx).Contains(findText) AndAlso rLines(idx).Trim().StartsWith(findText) Then
                            Return idx
                        End If
                    Next
                Else
                    If startIdx < 0 Then
                        startIdx = rLines.Count - 1
                    End If
    
                    If endIdx < 0 Then
                        endIdx = 0
                    End If
    
                    For idx As Integer = startIdx To endIdx Step -1
                        If rLines(idx).Contains(findText) AndAlso rLines(idx).Trim().StartsWith(findText) Then
                            Return idx
                        End If
                    Next
                End If
    
                Return -1
            End Function
    
            ''' <summary>
            ''' Find the next Attribute line
            ''' </summary>
            ''' <param name="startIdx"></param>
            ''' <param name="endIdx"></param>
            ''' <returns></returns>
            Private Function FindAttributeLine(startIdx As Integer, endIdx As Integer) As Integer
                If startIdx < 0 Then
                    startIdx = 0
                End If
    
                If endIdx < 0 Then
                    endIdx = rLines.Count - 1
                Else
                    endIdx = Math.Min(endIdx, rLines.Count - 1)
                End If
    
                For idx As Integer = startIdx To endIdx
                    If rLines(idx).Contains("DllExportAttribute::.ctor") AndAlso rLines(idx).Trim().StartsWith(".custom instance void ") Then
                        Return idx
                    End If
                Next
    
                Return -1
            End Function
    
            ''' <summary>
            ''' Find the line that contains the specified text
            ''' </summary>
            ''' <param name="findText"></param>
            ''' <param name="startIdx"></param>
            ''' <param name="endIdx"></param>
            ''' <returns></returns>
            Private Function FindLineContains(findText As String, forward As Boolean, startIdx As Integer, endIdx As Integer) As Integer
                If forward Then
                    If startIdx < 0 Then
                        startIdx = 0
                    End If
    
                    If endIdx < 0 Then
                        endIdx = rLines.Count - 1
                    Else
                        endIdx = Math.Min(endIdx, rLines.Count - 1)
                    End If
    
                    For idx As Integer = startIdx To endIdx - 1
                        If rLines(idx).Contains(findText) Then
                            Return idx
                        End If
                    Next
                Else
                    If startIdx < 0 Then
                        startIdx = rLines.Count - 1
                    End If
    
                    If endIdx < 0 Then
                        endIdx = 0
                    End If
    
                    For idx As Integer = startIdx To endIdx Step -1
                        If rLines(idx).Contains(findText) Then
                            Return idx
                        End If
                    Next
                End If
    
                Return -1
            End Function
    
            ''' <summary>
            ''' Get a string padded with the number of spaces
            ''' </summary>
            ''' <param name="tabCount"></param>
            ''' <returns></returns>
            Private Function TabString(tabCount As Integer) As String
                If tabCount <= 0 Then Return String.Empty
    
                Dim sb As New StringBuilder()
    
                sb.Append(" "c, tabCount)
                Return sb.ToString()
            End Function
    
            ''' <summary>
            ''' Write an informational message
            ''' </summary>
            ''' <param name="info"></param>
            Private Sub WriteInfo(info As String)
                If rVerboseOn Then
                    Console.WriteLine(info)
                End If
            End Sub
    
            ''' <summary>
            ''' Write an informational message
            ''' </summary>
            Private Sub WriteError(msg As String)
                Console.WriteLine(msg)
            End Sub
    
    #End Region
        End Class
    End Namespace

    There’s really nothing tricky or earth-shattering here. Mainly calls to ILDASM and ILASM, and quite a lot of string parsing (looking for the DLLExportAttribute markers and replacing them with the applicable IL code).

    The DLLExport Marker Attribute

    As for that DLLExportAttribute, its definition is much simpler:

    Imports System.Runtime.CompilerServices
    Imports System.Runtime.InteropServices
    
    Namespace DllExport
        ''' <summary>
        ''' Attribute added to a static method to export it
        ''' </summary>
        <AttributeUsage(AttributeTargets.Method)> _
        Public Class DllExportAttribute
            Inherits Attribute
    
            ''' <summary>
            ''' Constructor 1
            ''' </summary>
            ''' <param name="exportName"></param>
            Public Sub New(exportName As String)
                Me.New(exportName, System.Runtime.InteropServices.CallingConvention.StdCall)
            End Sub
    
            ''' <summary>
            ''' Constructor 2
            ''' </summary>
            ''' <param name="exportName"></param>
            ''' <param name="callingConvention"></param>
            Public Sub New(exportName As String, callingConvention As CallingConvention)
                _ExportName = exportName
                _CallingConvention = callingConvention
            End Sub
            Private _ExportName As String
    
            ''' <summary>
            ''' Get the export name, or null to use the method name
            ''' </summary>
            Public ReadOnly Property ExportName() As String
                Get
                    Return _ExportName
                End Get
            End Property
    
            ''' <summary>
            ''' Get the calling convention
            ''' </summary>
            Public ReadOnly Property CallingConvention() As String
                Get
                    Select Case _CallingConvention
                        Case System.Runtime.InteropServices.CallingConvention.Cdecl
                            Return GetType(CallConvCdecl).FullName
    
                        Case System.Runtime.InteropServices.CallingConvention.FastCall
                            Return GetType(CallConvFastcall).FullName
    
                        Case System.Runtime.InteropServices.CallingConvention.StdCall
                            Return GetType(CallConvStdcall).FullName
    
                        Case System.Runtime.InteropServices.CallingConvention.ThisCall
                            Return GetType(CallConvThiscall).FullName
    
                        Case System.Runtime.InteropServices.CallingConvention.Winapi
                            Return GetType(CallConvStdcall).FullName
                        Case Else
    
                            Return ""
                    End Select
                End Get
            End Property
            Private _CallingConvention As CallingConvention
    
        End Class
    End Namespace

    What Next?

    So, now that we can easily expose entry points from a .net assembly, what can we do with that?

    For starters, as my previous post mentioned, I’ve built a .net plugin for the MAME front end called Mala. It’s still in very early stages, and is not yet available, but it definitely works.

    Even more interesting, I’ve experimented with creating self-registering COM .net assemblies. But that will have to wait for another posting.

    For the full project, and a pre-compiled DLLExport.exe file, grab this zip.

    And definitely let me know how you use it!

    Collapsing Date Ranges in T-SQL

    0
    Filed under Code Garage, SQL

    imageI’ve been working on contract for a month or so now, helping to speed up some back end database summarization activity that had gotten slow enough that it was threatening to bleed into the next day’s time frame. Yuck!

    Mostly standard stuff, tweaking indexes, ditching cursors, etc.

    But one problem had me scratching my head today.

    Essentially, I had a table of Clients, each one of which could be linked to any number of “Exposure” records, each of those having a start and stop date of exposure.

    The trick was to determine how many total years of exposure a client had.

    The thing is, each client might have multiple exposure records with overlapping (or not) time frames. So essentially, the problem boiled down to collapsing all the exposures to a single sequential list of non-overlapping exposure timeframes. From there, it’s trivial to just add up the differences of the date for each time frame.

    But how to get there?

    Cursors

    The existing code was working fine, but took upwards of 40+ minutes. Essentially, it worked via cursors and functions (with more cursors) to collect all the years of all the timeframes for each client, convert them to a list of singular year elements, then convert that to a recordset and finally count up the entries. Workable, but terribly slow.

    Skinning the Cat

    I’d done something similar ages ago for a medical billing system, so I knew this kind of manipulation could be fast. But I’d long since forgotten exactly how I’d done it.

    However, a few Google searches and I landed on Peter Larsson’s blog post about collapsing date ranges using what he calls the “Clustered Index Update”. It’s 3 years old, but definitely something worth throwing in your bag of SQL tricks!

    First, create some test data:

    create table #test(
       id int,
       seq int,
       d1 datetime,
       d2 datetime)
    
    insert into #test
    select 1, null, '2005', '2006' union all
    select 1, null,'2007', '2009' union all
    select 2, null,'2001', '2006' union all
    select 2, null,'2003', '2008' UNION ALL
    SELECT    3, null,'2004', '2007' UNION ALL
    SELECT    3, null,'2005', '2006' UNION ALL
    SELECT    3, null,'2001', '2003' UNION ALL
    SELECT    3, null,'2002', '2005' UNION ALL
    SELECT    4, null,'2001', '2003' UNION ALL
    SELECT    4, null,'2005', '2009' UNION ALL
    SELECT    4, null,'2001', '2006' UNION ALL
    SELECT    4, null,'2003', '2008'

    Next, make sure you have a clustered index across the ID and both Date fields:

    CREATE CLUSTERED INDEX ix_id ON #test (ID, d1, d2) with fillfactor = 95

    Be sure that the SEQ field is initialized to NULL or 0 (already done via the population code above).

    Then, create several variables to assist with counting through the records to set the SEQ field. Use a SELECT to initialize those variables:

    DECLARE    
        @id INT,
        @Seq INT,
        @d1 DATETIME,
        @d2 DATETIME
    SELECT TOP 1
        @Seq = 0,
        @id = id,
        @d1 = d1,
        @d2 = d2
    FROM #test
    ORDER BY id, d1

    The Trick

    Finally, update the SEQ column using the “Clustered Index Update” trick:

    UPDATE #test
    SET   
        @Seq = CASE
            WHEN d1 > @d2 THEN @Seq + 1
            WHEN id > @id THEN @Seq + 1
            ELSE @Seq
            END,
        @d1 = CASE
            WHEN d2 > @d2 THEN d1
            WHEN id > @id THEN d1
            ELSE @d1
            END,
        @d2 = CASE
            WHEN d2 > @d2 THEN d2
            WHEN id > @id THEN d2
            ELSE @d2
            END,
        Seq = @Seq,
        @id = id

    Essentially, what’s happening here is that since the update doesn’t specify an order, SQL will update via the physical order in the database, which is the same as the clustered index (a clustered index determines the physical ordering of records in the table). And since the records are ordered in ID, D1, D2 order, the SEQ column will be updated with an incrementing number that effectively clusters overlapping ranges together.

    Since the records are already physically in that order, this update happens lightning fast because there’s no need to perform any index lookups.

    You can see the end result by selecting all the records at this point:

    select * from #test

    Now, the data is ready, you just have to query it using that SEQ column. For instance, this SELECT will retrieve the start and end date of each non-overlapping cluster of dates belonging to each ID.

    SELECT      
        ID,
        MIN(d1) AS d1,
        MAX(d2) AS d2
    FROM #test
    GROUP BY id, Seq
    ORDER BY Seq

    Mr. Larsson also describes a query to retrieve the “gaps” (or missing date ranges), which could be handy for a different class of problem.

    If, like me, you also need a grand total of the number of years, first, you can get the years in each collapsed timeframe and then get the grand total years, like this:

    select 
        ID,
        Years = Sum(Years)
    From (
        SELECT     
            ID,
            Years=Year(MAX(d2)) - Year(Min(d1)) + 1
        FROM #test
        GROUP BY id, Seq
        ) a
    Group by id
    Order by id    

    Using this trick took this particular query (actually a sizable set of queries and cursor loops) from 40+ minutes to under a minute, with virtually all of that minute being spent filtering down the set of records that I needed to specifically collapse the timeframes on (in other words, doing stuff unrelated to actually collapsing the date ranges). In all, several million records being processed in a few seconds now.

    Good stuff.

    Integrating with the New Office Backstage from a VSTO 3 Addin

    11
    Filed under .NET, Code Garage, Office, VSTO, Word

    If you’re like me and want to drop straight to the code, you can download the sample project here.

    When it comes to writing addins for Office applications, you really only have 2 choices.

    • Old School – Implementing the IExtensibility2 interface
    • VSTO

    The problem is that VSTO addins tend to be one trick ponies. If you want to write a single addin that works in multiple Office applications, or if you want a single addin to work across multiple Office Application versions, you’ll likely run into walls with VSTO. Sure, you could create separate DLL’s for each Office app, and for each target version, but good lord, who wants to do that?

    Still, VSTO makes dealing with Ribbons and TaskPanes much easier and can be handy when you need to target a single Office app, say, Word, and when you’re specifically concerned with a single version of the app (or maybe the latest few).

    This was the case recently with an addin I was working on.

    The Situation

    The target application was Word, specifically Word 2010. However, virtually all of the addin was finished by the time VSTO 4 and VS2010 was released. Since we didn’t really want to run the risks of retooling the addin for VSTO 4 (seeing as it’s a brand new platform and we were close to the end of the dev cycle), the decision was made to stick with VSTO 3.

    No big deal. VSTO 3 addins run perfectly fine under Word 2010.

    The Snag

    During the ramp up on Word 2010, however, we discovered the new Backstage view:

    image

    It’s a really nice extension to the traditional Office File menu. One of the few new elements of Office 2010 that really makes upgrading worthwhile, in my opinion, but that’s another story.

    At any rate, it turns out that the Backstage view is extensible via XML, very much like the Ribbon.

    Unfortunately, VSTO 3 has no support for the Backstage view.

    Customizing the BackStage in the First Place

    For me, though, the first question was exactly how do you customize the BackStage? Turns out, it’s fairly simple. If you’ve manually customized the Ribbon before, you’ll recognize the process instantly. John Durant wrote up a short and sweet article on the process here.

    Unfortunately, he describes modifying the BackStage by altering the Custom XML package in a document. This makes the BackStage modification document specific. Not the way you want to do it for an addin.

    I also found this great article, but it describes modifying the BackStage by implementing an addin using the older style IExtensibility2 model, not VSTO.

    More articles, same shortcomings. This was starting to look as hopeless as the Cowboy’s season this year.

    I knew that modifying the ribbon via an IExtensibility2 addin was as simple as creating a public function called GetCustomUI, and returning the XML for whatever custom buttons you want added to the Word Ribbon. From the articles mentioned above, it was obvious that the exact same process was used to alter the BackStage. The problem was, there wasn’t any obvious way to get at  the GetCustomUI function in a VSTO addin. All of that XML munging is automagically handled for you by VSTO.

    Further, I wanted, if at all possible, to continue to leverage the VSTO Ribbon support. The Visual Studio Ribbon Designer is just too handy to have to go back to manually crafting up the XML for it.

    However, I knew that no matter what happened, I was still going to have to manually build the XML for my BackStage customizations. That was ok, though.

    The Wrong Way – Wrap RibbonManager

    I initially thought I could just create a new object that wrapped RibbonManager.

    The main VSTO Connect class (that itself inherits from the VSTO Addin class), exposes the overridable function CreateRibbonExtensibilityObject  that expects a IRibbonExtensibility object as a return value.

    So, create a new object, RibbonManagerInterceptor, that implements IRibbonExtensibility, and just wraps the internally created RibbonManager object, and return that, like so:

    Private _RibbonExt As Microsoft.Office.Core.IRibbonExtensibility
    Protected Overrides Function CreateRibbonExtensibilityObject() As Microsoft.Office.Core.IRibbonExtensibility
        Dim RibbonManager = MyBase.CreateRibbonExtensibilityObject()
        _RibbonExt = New RibbonManagerInterceptor(RibbonManager)
        Return _RibbonExt
    End Function

    Unfortunately, it won’t work. The VSTO authors make the assumption that the object implementing IRibbonExtensibility is of base type RibbonManager. For instance:

    Private ReadOnly Property RibbonExtensibility As IRibbonExtensibility
        Get
            If (Me.ribbonExtensibility Is Nothing) Then
                Me.ribbonExtensibility = Me.CreateRibbonExtensibilityObject
                Dim ribbonExtensibility As RibbonManager = TryCast(Me.ribbonExtensibility,RibbonManager)
                If (Not ribbonExtensibility Is Nothing) Then
                    ribbonExtensibility.ServiceProvider = MyBase.HostContext
                End If
            End If
            Return Me.ribbonExtensibility
        End Get
    End Property

    This is from the Addin class in Microsoft.Office.Tools.Common.v9.0. You’ll notice that the property accepts an IRibbonExtensibility object, but then proceeds to cast it as RibbonManager and if that fails, the addin won’t register the service provider.

    What’s worse is that RibbonManager has been marked NotInheritable, so you can’t create a subclass from it to pass this test. Full stop.

    Intercepting GetCustomUI

    Boiling the basic requirements of an Office addin down, I knew I had to implement IRibbonExtensibility and the GetCustomUI method.

        Public Function GetCustomUI(ByVal RibbonID As String) As String Implements IRibbonExtensibility.GetCustomUI
            Dim xml = _RibbonManager.GetCustomUI(RibbonID)
    
            If _Connect.Core.WordInstance.Version = "14.0" Then
                '---- only add in backstage support for version 14 (Office 2010)
                Dim bs = <backstage>
                             <tab id="bsSample" label="BackStageSample" insertAfterMso="TabInfo">
                                 <firstColumn>
                                     <group id="bsSampleGroup" label="BackStage Sample Group">
                                         <topItems>
                                             <button id="BackStageSample"
                                                 label="BackStage Sample Button"
                                                 onAction="bsSampleClicked"/>
                                         </topItems>
                                     </group>
                                 </firstColumn>
                             </tab>
                         </backstage>
                xml = xml.Replace("</customUI>", bs.ToString & "</customUI>")
                xml = xml.Replace("http://schemas.microsoft.com/office/2006/01/customui", "http://schemas.microsoft.com/office/2009/07/customui")
            End If
            Return xml
        End Function

    The first step is to use the underlying RibbonManager object to generate its version of the CustomUI XML. That allows me to continue to use the Ribbon Designer in Visual Studio and the RibbonManager for all the heavy lifting where the Ribbon is concerned.

    Next, if I see the addin is hosted by Word v14 (2010), I use the handy inline XML feature of VB.net to create the BackStage custom XML and inject it into the Ribbon XML previously already generated.

    And finally, I have to patch the schema used, so that Word knows I’m defining BackStage customizations in the XML as well as Ribbon customizations.

    A Better Way

    I knew that VSTO was somehow intercepting  all callbacks from Word as defined in the CustomUI XML, and then generating events for the various Ribbon controls, or interrogating control properties. Maybe there was a way to hook into that process.

    So, I loaded up Reflector and started spelunking.

    It didn’t take long to realize what was going on.

    When you set up a custom UI for Word, you define callback functions in the XML that Word will then call when necessary. Those functions must be public functions on the object that implements IRibbonExtensibility and that is exposed as a COM object. Further, those functions are ALWAYS called via IDispatch. For instance, take the following custom UI:

    <backstage>
        <tab id="bsSample" label="BackStageSample" insertAfterMso="TabInfo">
            <firstColumn>
                <group id="bsSampleGroup" label="BackStage Sample Group">
                    <topItems>
                        <button id="BackStageSample"
                                label="BackStage Sample Button"
                                onAction="bsSampleClicked"/>
                    </topItems>
                </group>
            </firstColumn>
        </tab>
    </backstage>

    The onAction element above defines a function called bsSampleClicked, that Word will call on the IRibbonExtensibility object whenever that button is clicked.

    Interestingly, it doesn’t matter that this is for a BackStage object (a button) and not a Ribbon. Word vectors everything through that IRibbonExtensibility object.

    So, how does that process work then?

    Well, as it turns out, it’s not terribly complicated, but it does require a little work. With .NET, you implement a latebound IDispatch type call by implementing the IReflect interface. That interface contains a number of members that need to be implemented, though most can be stubbed out.

    However, you’ll definitely need to implement the GetMethods and InvokeMember functions.

    GetMethods returns to the caller an array of MethodInfo objects that describe the all latebound methods that our IRibbonExtensibility object will be supporting. Since we want to continue to allow the RibbonManager to service all of the methods that it needs to handle, you first need to retrieve the RibbonManager’s list of supported methods and then add to it:

        Private Function IReflect_GetMethods(ByVal bindingAttr As BindingFlags) As MethodInfo() Implements IReflect.GetMethods
            Dim ir = DirectCast(_RibbonManager, IReflect)
            Dim r = ir.GetMethods(bindingAttr)
            ReDim Preserve r(UBound(r) + 1)
            Dim mi = BackstageMethodInfo.CheckBoxActionMethod(DirectCast(_RibbonManager, RibbonManager), "bsSampleClicked", Nothing)
            r(UBound(r)) = mi
            Return r
        End Function

    Finally, to vector the method call properly, define the InvokeMethod method like so:

        Private Function IReflect_InvokeMember(ByVal name As String, ByVal invokeAttr As BindingFlags, ByVal binder As Binder, ByVal target As Object, ByVal args As Object(), ByVal modifiers As ParameterModifier(), ByVal culture As CultureInfo, ByVal namedParameters As String()) As Object Implements IReflect.InvokeMember
            Dim r As Object
            If name.StartsWith("bs") Then
                '---- it's a Backstage control, just intercept and pass through
                '     to the connect object
                Select Case name.ToLower
                    Case "bssampleclicked"
                        _Connect.bsSampleClicked()
                    Case Else
                End Select
                Return Nothing
            Else
                Try
                    Dim ir = DirectCast(_RibbonManager, IReflect)
                    r = ir.InvokeMember(name, invokeAttr, binder, _RibbonManager, args, modifiers, culture, namedParameters)
                Catch ex As Exception
                    Throw New TargetInvocationException(ex)
                End Try
                Return r
            End If
        End Function

    Here, if the callback function Word is trying to call starts with “bs”, I assume it’s one of my “backstage” callbacks and I drop into the first Select Case.

    If not, I forward the call on through to underlying RibbonManager object, so VSTO can continue to work it’s magic.

    The actual function that handles the callback, I defined in my VSTO Connect object:

        '---- Testing function
        Friend Sub bsSampleClicked()
            MsgBox("BackStage Sample Button was Clicked")
        End Sub

    A Few Side Notes

    I put together a small sample addin for Word that illustrates this approach. Download it here.

    If you download and unzip the sample project, one of the first things you’re likely to notice is the References folder. For addins like this, I like to copy any referenced dll locally to the project and reference it from there, rather than scattering referenced DLLs all over my system. It makes moving the project to other machines much easier, among other benefits.

    You’ll also notice that the DLLs are for Office 2007, as that’s part of the point of this sample; to show that a VSTO 3 addin  can target both 2007 and 2010 and support Ribbons and the Backstage.

    One downside to this approach is that VSTO 3 insists  that Office 2007 be installed, even if you’re actually only targeting 2010. You’ll get errors and very specific warnings to that effect if you try to run the project on a machine without Office 2007.

    Just make sure you’ve installed Office 2007, then Office 2010, to be able to run the addin against both versions.

    COM Visibility

    Since addins, even VSTO addin’s, are, at their core, COM dlls, you’ll likely find yourself having to deal with COM interop to some degree. To make that easier, I usually turn OFF COM visibility for the WHOLE PROJECT in the Assembly.vb file:

    ' Setting ComVisible to false makes the types in this assembly not visible 
    ' to COM components.  If you need to access a type in this assembly from 
    ' COM, set the ComVisible attribute to true on that type.
    <Assembly: ComVisible(False)> 

    And then turn ON COM visibility for each class that actually needs to be visible via COM.

    ''' <summary>
    ''' Core object for this addin
    ''' </summary>
    ''' <remarks></remarks>
    <ComVisible(True)> _
    <Guid("8618E3FB-D57B-4875-ABE4-D204E1C1046A")> _
    Public Class Core
    ...

    Debugging

    To make the project easy to debug, I always set the start action to “Start External Program”, and point it at my WinWord.exe in the Office installation.

    image

    You may need to change the path to WinWord as applicable to your system.

    Sample Project Requirements

    To run the sample project, you’ll need the following:

    • Visual Studio 2008
    • Office 2007
    • To test the backstage support, you’ll also need Office 2010 installed (They can be installed side by side except for Outlook).
    • Visual Studio Tools for Office 3.0 (VSTO). I found the installer already on my system at

      c:\Program Files (x86)\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages\VSTOR30\vstor30.exe
    • You’ll likely also want the service pack for VSTO. It was located here:

      c:\Program Files (x86)\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages\VSTOR30\vstor30sp1-KB949258-x86.exe

    The Wrapup

    So there you have it. Support for the latest Backstage View in a VSTO 3 based addin. It required a fair bit of poking around under the covers, something that would simply not be possible without Lutz Roeder’s excellent Reflector tool. If you don’t have that in your toolbox, you’re missing out on a fantastic debugging, diagnostic, research, and discovery tool!

    In the end, if you need your addin to support multiple Office applications or a wide variety of versions, your best bet will be to go with IExtensibility2 (or possible AddinExpress, though I’ve never used it, and have no idea whether it truly makes things easier for multi targeting or not).

    And finally, the standard disclaimer. IWOMM (It works on my machine). If you find bugs or problems, please let me know. Heck, if you know a better way (short of converting to VSTO 4!), I’d love to hear about it! I’m fairly certain that this isn’t the only way to skin this particular cat.

    Normal Ol’ DLLs from VB.net

    6
    Filed under .NET, Arcade, Code Garage, MSBuild, VB Feng Shui

    Every once in a while, I find a need to do something a bit off the wall. Recently, I had another one of those situations.

    I’ve spent a lot of time working with some of the old arcade emulators that are floating around (the most famous of which is MAME, or Multi Arcade Machine Emulator).

    Mame itself is pretty utilitarian, so there are a number of front ends  that are essentially menuing systems to provide a user with an easy to browse interface for selecting games to play and among the more popular front ends is MaLa.

    image

    MaLa Main screen (using one of many available skins) showing list of games, and a screenshot of the selected game

    One nice aspect of MaLa is that it supports plugins, and there are a number of them out there, to control LED lights, play speech, etc.

    I had had a few ideas about possible MaLa plugins for awhile, but the MaLa plugin architecture centers around creating a standard Win32 DLL with old fashioned C styled Entrypoints, and, well, I kinda like working in VB.net these days.

    Gone Hunting

    Eventually, curiousity got the better of me, and I started looking for ways to expose standard DLL entry points from a .net assembly. I ended up finded Sevin’s CodeProject entry called ExportDLL that allowed just that. Essentially, it works by:

    1. You add a reference in your project to a DLL he created, that only contains a single Attribute for marking the functions you want to export.
    2. Create the functions you want to export as shared functions in a MODULE
    3. You mark those functions with the Attribute
    4. You compile your DLL
    5. You then run ExportDLL against your freshly compiled DLL
    6. ExportDLL then decompiles your DLL into IL, tweaks it, and recompiles the IL code back into a DLL

    It sounds complicated but it’s really not.

    I set it all up and had things working in about 30 minutes.

    Gone South

    Unfortunately, all was not quite right. MaLa requires 2 entry points (among a host of them) defined with a single integer argument passed on the stack. Pretty simple stuff. So I coded up:

        <ExportDLL("MaLaOrientationSwitch", CallingConvention.Cdecl)> _
        Public Shared Sub EntryPoint_MaLaOrientationSwitch(ByVal Orientation As Integer)

    But when I ran the DLL within MaLa, it crashed immediately after calling this function, even with NO CODE in the function itself.

    What this meant is that something about the export process was trashing the stack. I spent a solid day hunting for clues as to what might be failing. I did find that eliminating the argument from the exposed entrypoint allowed MaLa to work properly AND call my entrypoint, but, being unable to get the passed Orientation value, the call was basically useless.

    Gone Around Again

    In digging through it all, I happened to notice a comment on the CodeProject page for Sevin’s article pointing to a similar library by Robert Giesecke. I’m not sure if the two were developed independently or not, but Robert’s is certainly a more polished set of deliverables. He even went so far as to put together a C# project template that makes it ridiculously easy to kick off a project using his technique.

    It turns out, not only is Robert’s approach cleaner, it actually properly exports the MaLaOrientationSwitch function above with no problems. MaLa can call it, pass in the argument and all is good.

    Gone Fishing

    One big difference between the two techniques is the Robert actually defines an MSBuild targets file to patch his DLL directy into the Visual Studio build process. Very cool! But, his build step happens AFTER the PostBuildEvent target, and it was in that target that I’d setup some commands to copy the DLL into a file called *.MPLUGIN, which is what MaLa specifically looks for. Hooking that process into the build itself makes debugging things quite natural, but, Mr. Giesecke’s target wasn’t allowing for that.

    Here’s Robert’s targets file:

    <Project
      xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <UsingTask TaskName="RGiesecke.DllExport.MSBuild.DllExportTask"
                 AssemblyFile="RGiesecke.DllExport.MSBuild.dll"/>
      <Target Name="AfterBuild"
              DependsOnTargets="GetFrameworkPaths"
              >
          <DllExportTask Platform="$(Platform)"
                       PlatformTarget="$(PlatformTarget)"
                       CpuType="$(CpuType)"
                       EmitDebugSymbols="$(DebugSymbols)"
                       DllExportAttributeAssemblyName="$(DllExportAttributeAssemblyName)"
                       DllExportAttributeFullName="$(DllExportAttributeFullName)"
                       Timeout="$(DllExportTimeout)"
                       KeyContainer="$(KeyContainerName)$(AssemblyKeyContainerName)"
                       KeyFile="$(KeyOriginatorFile)"
                       ProjectDirectory="$(MSBuildProjectDirectory)"
                       InputFileName="$(TargetPath)"
                       FrameworkPath="$(TargetedFrameworkDir);$(TargetFrameworkDirectory)"
                       LibToolPath="$(DevEnvDir)\..\..\VC\bin"
                       LibToolDllPath="$(DevEnvDir)"
                       SdkPath="$(FrameworkSDKDir)"/>
      </Target>
    </Project>

    I’d worked with MSBuild scripts before, so I knew what it was capable of, I just couldn’t remember the exact syntax. A few google searches jogged my memory, and I ended up here at a great post describing exactly how you can precisely inject your own targets before or after certain other predefined targets.

    I modified Robert’s targets file and came up with this:

    <Project
        xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
        <UsingTask TaskName="RGiesecke.DllExport.MSBuild.DllExportTask"
                 AssemblyFile="RGiesecke.DllExport.MSBuild.dll"/>
    
      <!-- Add to the PostBuildEventDependsOn group to force the ExportDLLPoints
           target to run BEFORE any post build steps (cause it really should) -->
      <PropertyGroup>
        <PostBuildEventDependsOn>
          $(PostBuildEventDependsOn);
          ExportDLLPoints
        </PostBuildEventDependsOn>
      </PropertyGroup>
    
      
      <Target Name="ExportDLLPoints"
              DependsOnTargets="GetFrameworkPaths"
              >
        <DllExportTask Platform="$(Platform)"
                       PlatformTarget="$(PlatformTarget)"
                       CpuType="$(CpuType)"
                       EmitDebugSymbols="$(DebugSymbols)"
                       DllExportAttributeAssemblyName="$(DllExportAttributeAssemblyName)"
                       DllExportAttributeFullName="$(DllExportAttributeFullName)"
                       Timeout="$(DllExportTimeout)"
                       KeyContainer="$(KeyContainerName)$(AssemblyKeyContainerName)"
                       KeyFile="$(KeyOriginatorFile)"
                       ProjectDirectory="$(MSBuildProjectDirectory)"
                       InputFileName="$(TargetPath)"
                       FrameworkPath="$(TargetedFrameworkDir);$(TargetFrameworkDirectory)"
                       LibToolPath="$(DevEnvDir)\..\..\VC\bin"
                       LibToolDllPath="$(DevEnvDir)"
                       SdkPath="$(FrameworkSDKDir)"/>
      </Target>
    </Project>

    Now, I can perform the compile, and execute my postbuild event to copy the DLL over to the MaLa Plugins folder and give it the requisite MPLUGIN name, all completely automatically.

    And, the icing on the cake is that I can build a MaLa plugin completely in VB.net, with no C or C# forwarding wrapper layer and with a fantastic XCOPY-able single DLL application footprint (save for the .net runtime, of course<g>).

    It’s a wonderful thing.

    Word’s Compatibility Options

    1
    Filed under .NET, Code Garage, Office, Word

    One element you’ll eventually run up against when dealing with Word documents is "compatibility”.

    image

    You can see one small indicator of compatibility in the above screenshot. When you open any old format DOC file in Word 2007 or 2010, it’ll open in compatibility mode.

    But what does that mean, really?

    Compatibility in a Nutshell

    Word’s Compatibility mode actually encompasses a fairly significant number of tweaks to how Word renders a document. You can see those options at the bottom of the Advanced tab on Word’s Options dialog.

    image

    a small sampling of compatibility options available

    Word sets those options in a number of different combinations depending on the source of the original document. You can select the source document manually via the below dropdown, but most of the time, Word will choose an appropriate selection automatically when it opens the original  document.

    image

    The problem is, many of those options can cause Word to render a document in very strange, unpredictable ways. In fact, most Word experts advise to manually turn OFF all compatibility options (and so force Word to layout the document using it’s most recent rules, either those set for Word 2007 or for Word 2010).

    Controlling Compatibility Programmatically

    Manipulating those settings manually via the Options dialog is fine, but if you’ve got thousands of documents to deal with, that may not be your best approach.

    Why not automate it with a bit of .net code?

        Private Sub ForceCompatibility(ByVal Doc As Word.Document)
            Doc.Convert()
    
            Doc.Application.Options.DisableFeaturesbyDefault = False
            For Each e As Word.WdCompatibility In [Enum].GetValues(GetType(Word.WdCompatibility))
                Dim res As Boolean = False
                Select Case e
                    Case Word.WdCompatibility.wdDontULTrailSpace
                        res = True
                    Case Word.WdCompatibility.wdDontAdjustLineHeightInTable
                        res = True
                    Case Word.WdCompatibility.wdNoSpaceForUL
                        res = True
                    Case Word.WdCompatibility.wdExpandShiftReturn
                        res = False
                    Case Word.WdCompatibility.wdLeaveBackslashAlone
                        res = True
                    Case Word.WdCompatibility.wdDontBalanceSingleByteDoubleByteWidth
                        res = True
                End Select
                Doc.Compatibility(e) = res
            Next
        End Sub

    So, what’s going on here?

    First, I pass in a Document variable to the function. This variable contains a reference to the Document object you need to fix compatibility on. You can easily obtain teh Active document object with the Application.ActiveDocument property.

    The function first calls the Doc.Convert method. This converts the document to the latest format available to the version of Word you’re running and enables all new features. This conversion happens in memory. Nothing is saved to disk at this point. Also, you’d think that this would be enough to turn off all compatibility tweaks, but alas, Word leaves many of them still turned of after this conversion.

    Next, it uses the DisableFeaturesbyDefault property to enable all available features.

    Then, it enumerates all the wdCompability options to clear or set them as necessary. This handy bit of trickery can come in handy in a number of situations. Essentially, it uses a little reflection here:

    [Enum].GetValues(GetType(Word.WdCompatibility))

    to retrieve an array of all the possible values of the wdCompatibility enumeration.

    Then, the For loop simply iterates through all the values in that array.

    The SELECT CASE then matches up a few specific enumeration values that don’t work quite like you might expect, and handles them with special cases.

    For instance, the wdNoSpaceForUL option is equivalent to the “Add Space for Underlines” option in the Word Options Dialog. However, the logic is reversed.

    Leave it to a word processing program to make use of double negatives like this!

    Odd Man Out

    The wdExpandShiftReturn option is the one exception. When cleared, this option prevents Word from expanding spaces on lines that end with a soft Return (a Shift-Return). Many Word experts indicate that this is preferable to Word’s default behavior of expanding those spaces, so I’ve set it here accordingly. Your mileage may vary.

    And finally, be sure the SAVE the document in xml format, using the Save method like this:

    Doc.SaveAs FileName:="doc.xml", FileFormat:=wdFormatXML

    So there you have it. A quick and easy way to force a document into the latest Word file format and turn off any errant compatibility options in the process.

    Code Garage – Escaping and UnEscaping XML Strings

    4
    Filed under Code Garage, XML

    image If you work much with XML, eventually, you’ll end up needing to take raw string data and either Escape it (replace characters that are illegal in XML in the string with legal XML markup) or UnEscape it (replace any XML markup in the string with the represented characters).

    For instance, a “>” (greater than) symbol is illegal in the content of an XML node, and must be represented by &gt;

    There are any number of approaches out there to handle this. Most involve simple brute force string search and replaces. They work, but I thought there must be a more elegant (and already thought out) solution to this problem.

    However, after quite a bit of searching, I gave up and wrote my own, making use of the XML functions already defined in the .net framework.

    To ENCODE a string into legal XML node content…

        Public Function EncodeXML(ByVal s As String) As String
            If Len(s) = 0 Then Return ""
            Dim encodedString = New StringBuilder()
            Dim writersettings = New System.Xml.XmlWriterSettings
            writersettings.ConformanceLevel = Xml.ConformanceLevel.Fragment
            Using writer = System.Xml.XmlWriter.Create(encodedString, writersettings)
                writer.WriteString(s)
            End Using
            Return encodedString.ToString
        End Function

    To reverse the process and Decode a string…

        Public Function DecodeXML(ByVal s As String) As String
            If Len(s) = 0 Then Return ""
            Dim decodedString As String = ""
            Dim readersettings = New System.Xml.XmlReaderSettings
            readersettings.ConformanceLevel = Xml.ConformanceLevel.Fragment
            Dim ms = New System.IO.StringReader(s)
            Using reader = System.Xml.XmlReader.Create(ms, readersettings)
                reader.MoveToContent()
                decodedString = reader.ReadString
            End Using
            Return decodedString
        End Function

    No, I haven’t performed any exhaustive performance measures on these. I’ve generally not used them in any kind of ultra high volume situations, but they’re clean, simple and leverage existing code that already performs the necessary functions.

    Configuring Log4Net in a .net VSTO Word Addin

    3
    Filed under Code Garage, Installations, log4net, VB Feng Shui

    VSTO (Visual Studio Tools for Office) is a great way to put together addins for the various MS Office applications (especially Word, Excel, and Outlook).

    And Log4Net is a fantastic and unbelievably flexible logging framework for .net applications.

    So naturally, I wanted to use them together.

    And doing so is not bad at all, till it came to configuration…

    A Disclaimer (with a note of Encouragement)

    Log4Net is not the most straightforward package out there. It’s extremely flexible, and very easy to work with once you’ve gotten used to it, but getting into the Tao of the thing took a few days, for me, anyway.

    Don’t let that discourage you. It really is a spectacular framework for handling virtually any aspect of logging in your applications. And it really doesn’t take much “setup code” at all to get it operational.

    The Super Highway

    The easiest way to configure log4net in a .net application (VSTO addins included) is to simply call Configure on the XMLConfigurator object:

    log4net.Config.XmlConfigurator.Configure()

    That’ll work, but unfortunately, since your VSTO addin is a DLL, log4net will, by default, look in the current app.config file, which, if you’re running in Word, for instance, will be WinWord.exe.config in the folder where WinWord.exe lives.

    Since WinWord.exe.config is Word’s config file, it’s probably not the best idea in the world to go shoe-horning your own (or log4net’s) config stuff in there as well. Not to mention how do you get your config information easily into that file during installation (or properly remove it during an uninstall).

    The Scenic Byway

    What you really want is for your VSTO addin DLL to have it’s own config file. Something that lives in the same folder as your DLL itself, and can easily be installed and removed.

    Sure enough, that Configure method has an overload that accepts a FileInfo structure for an arbitrary XML Config file. So you can just do this:

    Dim MyConfigFile = Me.GetType.Assembly.ManifestModule.Name & ".config"
    If My.Computer.FileSystem.FileExists(MyConfigFile) Then
        Dim fi = My.Computer.FileSystem.GetFileInfo(MyConfigFile)
        log4net.Config.XmlConfigurator.Configure(fi)
    End If

    What this does effectively is construct a filename based on the name of whatever assembly the current code is defined within, but with a “.config” extension.

    It then checks for the existence of that file. Since there’s no path on the file, it only looks in the current directory, but that’s fine, since the config file will always be located in the same folder as it’s DLL.

    And finally, if the file is found, it retrieves a FILEINFO object for it and configures Log4Net with that file.

    Bumps along the Road

    Unfortunately, that will get you farther, but not by much.

    There are two problems.

    1. In debug mode in the IDE, the “current directory” is, indeed, the same folder as the one with your addin DLL in it. But, when running in release mode, in production, with Visual Studio completely out of the picture, the current directory is very likely the folder where WinWord.exe is located. Not good.
    2. Worse, in production, VSTO addins are generally copied to a “shadow cache” folder by the .net framework, so that the original files can be upgraded in place easily while the application is in use.

    Unfortunately, your config will will not get copied to the shadow cache.

    This means that for determining where to look for your config file, using something like:

    • Me.GetType.Assembly.CodeBase or
    • Me.GetType.Assembly.Location

    won’t work, because often times, they’ll point you off into the wilds of the assembly cache folder and not  the \Program Files\MyCompany\MyProduct\ folder where you installed your addin and where you most likely would prefer your MyProduct.dll.config file to live.

    Happy Trails

    In the end, I found the best solution to be:

    1. Look in the “current directory” for your config file.
    2. If you find it, use it from there. This accommodated easy debugging while in the IDE because you can easily get to and edit your config file.
    3. If you don’t find it, construct the path to the app’s \Program Files\ folder and check there.
    4. If it’s not there either, just fall back to the default and call XMLConfigurator.Configure() with no parameters and let it default everything.

    A routine that puts all that together looks like this:

    Private Sub ConfigureLog4Net()
            Dim MyConfig = Me.GetType.Assembly.ManifestModule.Name & ".config"
            If Not My.Computer.FileSystem.FileExists(MyConfig) Then
                '---- not in current dir, so check in our Program Files folder
                Dim pth = Path.Combine(My.Computer.FileSystem.SpecialDirectories.ProgramFiles, My.Application.Info.CompanyName)
                pth = Path.Combinepth, My.Application.Info.ProductName)
                MyConfig = System.IO.Path.Combine(pth, MyConfig)
            End If
            If My.Computer.FileSystem.FileExists(MyConfig) Then
                Dim fi = My.Computer.FileSystem.GetFileInfo(MyConfig)
                log4net.Config.XmlConfigurator.Configure(fi)
            Else
                log4net.Config.XmlConfigurator.Configure()
            End If
    End Sub

    To use this, you’ll need to make sure that your installation package installs your addin DLL and it’s config file into the \Program Files\Company Name\Product Name folder.

    This is the pretty typical case, though, so that shouldn’t be a worry.

    Later on Down the Road

    This obviously begs the next question. What about other application settings? Log4Net reads stuff out of an arbtrary config file  you specify, but the My.Settings object does not. It’ll still end up looking in the default place, which is the host application’s config file (again, WinWord.exe.config, or Excel.exe.config, etc).

    I hope to cover a decent solution to that in a later post.

    Parsing Key-Value Pairs Via Regular Expressions

    5
    Filed under Code Garage, Regular Expressions, VB Feng Shui

    I’ve often found myself in need of a Key-Value pair parser. Simple stuff, really. Essentially, the idea is to be able to parse any of the following from a typical command buffer:

    Key=Value (no whitespace in the key or value)

    Key=”Value” (whitespace is ok in the value)

    Key (when just the existence of the key signals something)

    This sort of parser is fairly easy to write, but this time, I’d just finished playing with regular expressions for another parsing task, so I thought, why not give them a try here?

    After a few minutes with the Rad Regular Expression Designer, I’d put together what appeared to be a pretty robust expression for this.

    My version keys of the matches instead of the seperators. I did this mainly because I wanted the Key and Value parts to be returned as “cleanly” as possible. That means the Key should be just the Key, no whitespace or “=” and the value should never include the leading or trailing quote marks, if they’re there).

    The end result is a function that takes a string buffer and returns a generic Dictionary of Key Value string pairs.

    Imports System.Text.RegularExpressions
    
    
    Module RegEx
    
        Public Function ParseKeyValuePairs(ByVal Buffer As String) As Dictionary(Of String, String)
            Dim Result = New Dictionary(Of String, String)
    
            '---- There are 3 sub patterns contained here, seperated at the | characters
            '     The first retrieves name="value", honoring doubled inner quotes
            '     The second retrieves name=value where value can't contain spaces
            '     The third retrieves name alone, where there is no "=value" part (ie a "flag" key
            '        where simply its existance has meaning
            Dim Pattern = "(?:(?<key>\w+)\s*\=\s*""(?<value>[^""]*(?:""""[^""]*)*)"") | " & _
                          "(?:(?<key>\w+)\s*\=\s*(?<value>[^""\s]*)) | " & _
                          "(?:(?<key>\w+)\s*)"
            Dim r = New System.Text.RegularExpressions.Regex(Pattern, RegexOptions.IgnorePatternWhitespace)
    
            '---- parse the matches
            Dim m As System.Text.RegularExpressions.MatchCollection = r.Matches(Buffer)
    
            '---- break the matches up into Key value pairs in the return dictionary
            For Each Match As System.Text.RegularExpressions.Match In m
                Result.Add(Match.Groups("key").Value, Match.Groups("value").Value)
            Next
            Return Result
        End Function
    
    
        Public Sub Test()
            Dim s = "Key1=Value Key2=""My Value here"" Key3=Test Key4 Key5"
            Dim r = ParseKeyValuePairs(s)
            For Each i In r
                Debug.Print(i.Key & "=" & i.Value)
            Next
        End Sub
    End Module

    I’ve included a simple test function to help validate it.

    DISCLAIMER: I’m not RegEx guru, so there are likely much faster ways to assemble the Regex. If you have one, by all means please comment! And finally, it’s entirely possible that I’ve missed some examples of badly formed input that would cause weird parsing results.

    For instance, in the above example, notice that “Key3=Test Key4 Key5” will return Key3 set to “Test” and Key4 and Key5 set to empty strings.

    If the user meant for Key3’s value to be “Test Key4 Key5”, there would need to be quotes around the value.

    But, parsing issues like that will be the norm in any kind of parsing logic for formats such as this, so I’m not terribly worried about it.

    Implicit Casts in VB.net

    0
    Filed under Code Garage, VB Feng Shui

    I really hadn’t paid much attention to Implicit Casting in VB. I’d heard the term, thought it might be an interesting idea, but then completely forgot about it.

    But I was recently reading a blog post discussing the EntitySpaces ORM, and the author happened to offhandedly mentioned implicit casts with respect to certain features of that ORM. I was intrigued again, so I set off down that road.

    Come to find out, Implicit casts were one of the language features new in VS2005! (wow, how’d I miss that?) Essentially, they allow you to explicitly dictate what happens when you implicitly  cast one object to a different type.

    How’s that again?

    VB is rife with instances where you might need to implicitly cast one object to another of a different type. You’ve got the obvious situations, for instance, converting an object from a subtype to a super type (granted, not the most ideal example, but it illustrates the point):

    Dim d = New Dog
    Dim a As Animal = d

    And then there are more subtle examples. Here, I’ve created a Dog object with the name “Rover” but then I’m comparing it directly to a string. The implicit conversion is from Dog to String:

    Dim d = New Dog
    d.Name = “Rover”

    If d = "Rover" Then blah....

    Now, before anyone rails on me for encouraging bad programming practices, these are all contrived examples, just to point out the possibilities.

    The point is, if you find yourself needing to cast one object into another, and such casting makes logical sense, implicit casting provides a very clean, terse way to accomplish it.

    Widening or Narrowing?

    Any time you convert one object to another, there’s only 2 possible outcomes:

    • The conversion will always succeed. This is known as a Widening Conversion.
    • The conversion could succeed or fail. This is known as a Narrowing Conversion.

    A lot of documentation on the matter will describe a Widening Conversion as one that converts a derived type to one of it’s base types, and a Narrowing Conversion as one that converts a base type to a derived type, but in practice, the two types don’t have to be related at all.

    For instance, you can define either a Narrowing or a Widening conversion operator to convert from a StringBuilder Class to a String and vice versa, but neither type is directly related to the other.

    The Caveats

    There’s always at least one, right? Well, first, as you might guess, used with abandon, implicit casting can lead to code that bears a striking (and quite unwelcome) resemblance to old school VB code chock full of variants.

    But a less obvious snag is that when you cast, you need to pay special attention to whether you’re creating a new object or just recasting the existing object.

    For instance, you might define a Widening Conversion from Dog to Animal as:

    Public Shared Widening Operator CType(ByVal InitialData As Dog) As Animal
        Dim a = New Animal
        a.Name = InitialData.Name
        Return a
    End Operator

    But in actuality, a new Animal object with the same Name as the original Dog object is created. That might be what you want, or it might not be.

    Project Level Implicit Conversion

    If you’ve never noticed before, your project actually has an overall level of warning configuration for Implicit Conversion, because it can be such a nasty issue.

    You’ll find it on the Compile Tab of the project properties:

    image

    1. Set to None, VB won’t warn you at all when you attempt to implicitly convert types.
    2. Set to Warning, VB will show a warning in the Errors list, but will still allow the project to compile.
    3. Set to Error, VB won’t even allow the project to compile.

    Generally, you’ll want this set to Error. The good news, however, is that even with this option set to Error, VB will not consider those implicit conversions that are handled explicitly by a Widening Conversion as errors. If you think about it, this makes sense, because a Widening conversion, by its very nature, can’t fail, and thus really isn’t an implicit conversion anymore.

    StringBuilder Example

    Francesco Balena wrote up an excellent short article here that illustrates using a Widening conversion to make working with StringBuilder objects far easier. He uses Widening conversions to implicitly cast from StringBuilder to String and back, like so:

    Public Shared Widening Operator CType(ByVal op As StringBuilder6) As String Return op.ToString() End Operator Public Shared Widening Operator CType(ByVal str As String) As StringBuilder6 Dim op As New StringBuilder6() op.buffer.Append(str) Return op End Operator

    Since these are Widening Conversions, they won’t fail, and thus they won’t be flagged as errors even with Implicit Conversions set to Error in the project properties.