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

8
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!

%$^&@*~ Worm!

0
Filed under Rants

imageWell, I’ve been working professionally for a long time now and have only had a virus/worm problem once (I knew better than to open the FTP port through my firewall, but my wife needed to send a large file, I wouldn’t leave it open, and, what could happen? <ugh>)…

Anyway, fast forward to last Sunday, I’m doing a some research on a little project of mine when suddenly things are running really slowly.

I check a few of the usual suspects and nothing. So I open TaskMan, figuring I’ve got FireFox running rampant and chewing a bunch of ram.

Well it was, but that wasn’t what was interesting.

What was interesting?

What was interesting was that I was seeing a 20 character long randomly named EXE run, spawn another similarly named EXE, then die. Over and over again.

That’s not the behavior of any kind of normal application.

Yikes.

First things first.

After some frantic clicking trying to kill the process before it could spawn, I finally managed to. Ok. So now what. I opened FireFox and Googled “random named exe running”, got back a list of results and clicked on one.

I ended up at some link farm page (you know the kind, tons of links, but nothing worth anything).

Ok. Back up and try another result. Different link farm page, but same format.

Huh-Whuh?

Ok, Search for “CreateWindow”, first Google result is an MSDN page, click on it, same dang link farm page!

Crap. I’d been hijacked.

Reboot

Maybe it was just an in memory thing, so I reboot (and disconnect from my network!).

Once I log back in, everything crawling. Back in TaskMan and I see, literally, dozens of randomly named EXE’s spawning and dying all over the place.

Double Yikes!

Eset’s NOD32 is running, but it can’t keep up.

Shutdown.

Using another computer, I used NOD32 to create an emergency rescue USB drive.

Back to the infected machine, I boot to the thumb drive and start a scan. Oh…..Dear……Lord….. This is going to take a while.

Hours Later….

When it finally finished, it’d found a number of infected files, particularly, the Win32/Olmarik worm, and cleaned them, but I wasn’t convinced.

I did a quick check for any EXE’s dated that day, and sure enough, there were dozens of them, all about 2.4mb in size, in the SysWow64 folder. I deleted them all, then searched for any other files modified in the last 2 days that I didn’t immediately recognize. I deleted everything I found.

Restart and everything’s running lovely again.

Not out of the Woods

I start up FireFox and Google search results are still redirecting to various link farms. Crap.

I use another computer to search for solutions…

  1. Uninstall FireFox
  2. Delete everything related to FireFox on your drive.
  3. Uninstall Java
  4. Delete everything related to Java on my drive

Reboot and reinstall FF. Back and working normally. Phew! I just have to restore all my bookmarks and addins and I should be good there.

Turns out, the worm creates some Java hooks into your browser than causes the redirection. Nasty stuff.

Still Not Out of the Woods

I suspected it still wasn’t over and sure enough, I’m right. A few hours later, I happen to need to use KeePass for a password. Pressing Ctrl-Alt-A (standard KeePass hotkey), I get an Explorer popup saying that “The executable 43HJKAN5H1AVC.exe could not be located. Remove this shortcut?”

Son-of-a-bitch.

The damn worm created short-cut links to those random named exes I’d already deleted.

So off I go hunting down all the LNK files modified in the last few days and delete all of them.

Finally, I believe everything’s back to normal (though I’m still walking cautiously for now). I’ve since run a full computer scan on all my machines with nothing noted. Fingers crossed.

Post mortem

I’m still not completely sure what vector the worm used to get in, considering NOD32 was running the whole time. The only thing I can think of was that my daughter was in and out of my office at the time. I could have been distracted at some point and clicked a popup that I didn’t really intend to click.

But that’s just a guess.

The second thing that bothers me is why NOD32 didn’t catch this. It did catch at least parts of it, but the Java browser hijack totally slipped through. Doesn’t give me a good feeling…

Handy VS2010 Extensions

0
Filed under Subversion, Utilities, Version Control, Visual Studio

I’ve recently had to set up several machines for use with Visual Studio 2010 and found that I just about had to install several extensions, as they’re so useful in normal day-to-day programming.

They are:

  • The NuGet Package Manage (easily pull programming libraries into your project)
  • AnhkSVN for Visual Studio (great Subversion integration)
  • Productivity PowerTools (the “Locate in Solution Explorer” function alone makes this worth downloading)
  • VSCommands (the File Structure Viewer and enhanced syntax highlighting makes this one very nice)
  • DevColor (provided additional functions for setting colors in CSS/HTML files, right from the VS edit window)
  • IndentGuides (visualizes indents. Yeah, so you’re not REALLY supposed to have a for loop that spans pages of code, but hey, sometimes you have to read other people’s code, right? <g>)

There’s tons more extensions accessible from Tools/Extension Manage right inside Visual Studio, so if you haven’t browsed through it in a while, it might be worth a few minutes!

 

Oh, and, two more tools to throw out there. Grab a copy of the freeware Snarl from the guys at Fullphat and then download CommitMonitor, which connects to Snarl and provides you with toast notifications of checkins from other developers in Subversion projects that you care about. Handy stuff if your shop uses SVN.

Commenting out Blocks of XAML

16
Filed under Windows Phone 7

Just happened across a very handy trick for commenting out large blocks of XAML. If you’ve worked very much with XAML, you’ve probably run into the problem of wanting to comment out a large chunk, which might or might not already have XML style comments (<!– comment here –>).

If you’ve tried it, you’ve quickly discovered you can’t nest XML comments, which isn’t so much a XAML thing as it is an XML thing.

However, there is a fairly easy solution that works quite nicely.

First, in the declaration part of your XML file (up at the very top, where all the XMLNS elements are defined), add these lines:

1
2
3
4
5
6
7
8
9
10
<UserControl
    ....
 
    xmlns:c="comment" <-- NOTICE THIS LINE
    mc:Ignorable="d c"  <-- NOTICE THE "C" in this line
    d:DesignHeight="800" d:DesignWidth="424">

And finally, when you want to add a comment, instead of the <!– –> pair as you might normally, use this instead:

1
2
3
4
5
6
7
8
<c:comment>
   I'm commenting out the use of the text block below
    
   <!-- This text block should be commented -->
   <TextBlock />
 
    <c:comment>This is a nested comment</c:comment>
</c:comment>

Not only can you continue to use normal XML comments as usual, but, when necessary, you can easily (and obviously) comment out entire blocks, even if they already contain other comment elements, or XML comments.

And, by using the mc:Ignorable attribute, VS and Blend complete ignore the content of the comment elements.

Not perfect, but pretty dang close!

More Fun with Generic XAML Errors

0
Filed under Troubleshooting, Windows Phone 7

Here’s another obscure XAML error I hope no one else stumbles onto.

I have several VisualStates defined for a particular UserControl. One example is:

1
2
3
4
5
<VisualState x:Name="Waiting">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="pnlBeforeWakeup">
            <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="0" />
        </ObjectAnimationUsingKeyFrames>

Now, I’d entered that state by hand and it worked just fine in Visual Studio, compiles and runs (at least, on the phone emulator).

But, when I tried to fire up Expression Blend, the control would load initially, but when I tried to view the States tab, crash!

After a few false starts, I just removed all my VisualStates manually and recreated one via Expression Blend.

Presto! Anyone care to guess the “fault”?

<queue Jeopardy theme>

Ok, here’s the version that works fine in both VS and Blend.

1
2
3
4
5
6
7
8
9
<VisualState x:Name="Waiting">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="pnlBeforeWakeup">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Visible</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>

Notice that instead of Value=”0”, Blend seems to want a specific Value element, with a Visibility sub-element.

And finally, here’s the Expression error that results from using the Value attribute as I did originally.

image

The only clues that helped were the fact that it only happened when I tried clicking on a particular State that made use of the Value=”0” for Visibility, and the mention of “StoryBoard” in the StackTrace above.

“Unspecified Error” in XAML under WP7

1
Filed under Troubleshooting, Windows Phone 7

Ran into a very strange error today that took me a bit to figure out.

Essentially, I was templating a standard ol’ Silverlight Button (I wanted it to have a bit of a gradient and a nice rounded border in this instance).

I was mainly messing around with things, so I was defining the control template inside the button control itself, instead of the more general purpose way of defining a Style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Button Name="btnTest" Content="Caption" Click="btnTest_Click" HorizontalAlignment="Stretch" >
    <Button.Template>
        <ControlTemplate>
                <Border BorderBrush="White" BorderThickness="3" CornerRadius="10" Margin="{StaticResource PhoneMargin}" >
                    <Border.Background>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FFCDCDE5" Offset="0" />
                        <GradientStop Color="#FF389940" Offset="0.25" />
                        <GradientStop Color="#FF008d00" Offset="0.314" />
                        <GradientStop Color="#FF002200" Offset="1" />
                    </LinearGradientBrush>
                </Border.Background>
                <TextBlock Text="{TemplateBinding Content}" HorizontalAlignment="Stretch" TextAlignment="Center" FontFamily="Segoe WP" FontSize="{StaticResource PhoneFontSizeExtraLarge}" Margin="0,6,0,18"/>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>

Nothing special, but when I ran the program and navigated onto the page containing this button….

image

Ugh, Just about the most general error message possible.

Fortunately, I’d checked things in just a little earlier so I had a record of what had changed, the main thing being that I’d added the use of the TemplateBinding Content feature.

The idea there is to be able to customize exactly what UI element is used to display the “Content” property of the Button control itself. As you can see, I set the button’s Content to “Caption” and then bind to that in a TextBlock.

Simple stuff.

But it crashed.

If I took out the TemplateBinding reference, everything worked just fine.

After a lot of headscratching, I eventually realized that in this case, I did NOT provide a TargetType argument to ControlTemplate. I guess I was thinking that sense the ControlTemplate was being defined within an actual control instance, it wasn’t necessary. Lo, I was but mistaken!

Once I’d specified the TargetType for the ControlTemplate element, everything was back on track.

1
2
<Button.Template>
    <ControlTemplate TargetType="Button">

It truly is the tiny things in XAML that’ll get you!

Determining Whether You’re Running on the Windows Phone Emulator or Not

0
Filed under .NET, Windows Phone 7

This is a tiny little quick-tip for Windows Phone 7

I came up with a tiny little function to make it easy to check whether you’re running under the Windows Phone Emulator this afternoon.

1
2
3
4
5
6
7
8
9
10
11
Public Function IsEmulator() As Boolean
    Select Case Microsoft.Devices.Environment.DeviceType
        Case Microsoft.Devices.DeviceType.Device
            Return False
        Case Microsoft.Devices.DeviceType.Emulator
            Return True
        Case Else
            '---- just a fall back case
            Return True
    End Select
End Function

It’s trivial really, I know, but I often wrap these kinds of utility functions in an easy-to-remember function name, because remembering “Microsoft.Devices.Environment.DeviceType” is a lot more difficult than “IsEmulator” <g>.

Showing a Bing Map in Windows Phone 7.1 With Directions

0
Filed under .NET, Windows Phone 7

image

For a Windows Phone 7 app I’ve been playing with, one last element I wanted to include was a “directions” function. Basically, I just wanted to pop up a Bing Map with a start and end point and directions, much like you’d get if you went directly to the maps application on the phone and manually entered the start and end point.

The API for this is pretty straightforward, at least for WP7.1 (Mango).

Essentially, you create a BingMapsDirectionsTask, supply a start and end point and invoke Show.

1
2
3
4
Dim bmt = New BingMapsDirectionsTask()
bmt.Start = New LabeledMapLocation(StartName, New GeoCoordinate)
bmt.End = New LabeledMapLocation(EndName, New GeoCoordinate)
bmt.Show()

The problem was that New GeoCoordinate element. For the BingMapsDirectionsTask, you have to supply two LabeledMapLocation objects, one for the start and one for the end point.

No big deal, but according to the docs, you need to supply a Name for the labeled location, and a coordinate (essentially, just a Lat/Lon). This means that if all you have is an address, supplying the lat/lon becomes an issue of geocoding, and as far as I can tell, there’s no built-in service on the phone to geocode addresses.

I was able to turn up several good examples of using the BingMapGeoCodeService online, and they all worked fine (once you go through the requisite step of obtaining an AppID from www.bingmapsportal.com).

But, in the process of making that work, I discovered something interesting.

When creating the LabeledMapLocation objects for the start and end points, if you supply an address for the name, and nothing  for the GeoCoordinate, the BingMapsDirectionsTask will automatically geocode the address!

The Disclaimer

I’m not sure whether this is intentional behavior, as the available docs for BingMapsDirectionsTask are pretty sparse, and it could be that this won’t make it into the final 7.1 release.

But, it sure makes interacting with BingMaps for simple direction functionality ridiculously easy.

The final code, then, would look like this:

1
2
3
4
Dim bmt = New BingMapsDirectionsTask()
bmt.Start = New LabeledMapLocation("12 Main St, Dallas, Tx 76011", Nothing)
bmt.End = New LabeledMapLocation("500 Oak Lawn Ave, Dallas, Tx, 76011", Nothing)
bmt.Show()

Granted, using the BingMapGeoCodeService, you’ll have a lot more flexibility, including the ability to specify the level of confidence at which to filter geocoding hits, and you can actually retrieve multiple hit values and present them to the user in whatever fashion you want.

Let me know how it works for you!

Helpful Enum Description Attribute

2
Filed under .NET, VB Feng Shui

imageWhen you work with Enums, often, it’s nice to have a description to go along with the value. Of course, you can use reflection to retrieve the name of the enum, and that’s sufficient for many purposes. But sometimes, using the name ends up being awkward, or just won’t work.

For those times, I created an EnumDisplayName Attribute.

First, the attribute definition:

1
2
3
4
5
6
7
8
9
10
<AttributeUsage(AttributeTargets.Field)> _
Public Class EnumDisplayNameAttribute
    Inherits Attribute
 
    Public Property DisplayName As String
 
    Public Sub New(ByVal DisplayName As String)
        Me.DisplayName = DisplayName
    End Sub
End Class

Using it would then look something like this:

1
2
3
4
5
6
7
8
Public Class Core
    Public Enum ChimeSoundsEnum
        <EnumDisplayName("None")> _
        None = 0
        <EnumDisplayName("Air Raid")> _
        AirRaid = 1
        <EnumDisplayName("Beeper")> _
        Beeper = 2

Now, all you need is an extension method on Enum to read the DisplayName for a particular enum value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Imports System.Runtime.CompilerServices
 
    <Extension()> _
    Public Function DisplayName(ByVal e As [Enum]) As String
        Dim enumType = e.GetType
        If enumType.IsEnum Then
            Dim f = enumType.GetField(e.ToString())
            Dim displayNameAttribute = DirectCast(f.GetCustomAttributes(GetType(EnumDisplayNameAttribute), False).FirstOrDefault(), EnumDisplayNameAttribute)
 
            If displayNameAttribute IsNot Nothing Then
                Return displayNameAttribute.DisplayName
            End If
        End If
        Return [Enum].GetName(enumType, e)
    End Function

And finally, to read all the DisplayNames for a particular Enum type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Public Function GetEnumDisplayNames(ByVal et As Type) As String()
    If et.IsEnum Then
        Dim l = New List(Of String)
        Dim flds = From f In et.GetFields Where f.IsLiteral = True
        For Each f In flds
            Dim desc = DirectCast(f.GetValue(f), [Enum]).DisplayName()
            If Desc IsNot Nothing Then
                l.Add(desc)
            End If
        Next
        Return l.ToArray
    Else
        Return Nothing
    End If
End Function

Using that last function, you get back an array of DisplayNames that you can then databind to (for, say the ItemSource of a ListBox).

And finally, using these same techniques, you can create any number of other Enum metadata elements to make enums much more useful and easier to work with. You could have a ShortName attribute, a DataColumn attribute, maybe a PermissionLevel attribute, etc.

Simple No-Code Navigation in Windows Phone 7 Apps

0
Filed under Windows Phone 7

imageIn working with Windows Phone 7, one element that struck me as odd early on, is that the ApplicationBarIconButton (those “soft” buttons at the bottom of a normal WinPhone 7 page, highlighted in red in the screenshot) requires code in order to navigate. In other words, you MUST supply a CLICK event handler and in code behind, make use of the page’s NavigationService to perform the navigation.

Further, the URI to navigate to ends up in code behind as well.

Not ideal.

But there is a simple solution.

URIMapping

First, to make things really simple, you need to setup URI mapping. This allows your xaml to reference, say /Settings, and actually navigate to the URI /ui/SettingsPage.xaml?pvt=Main.

To do this, open up your App.xaml file and add something like this:

1
2
3
4
5
6
<Application.Resources>
    <!-- URIs for navigation purposes -->
    <nav:UriMapper x:Key="UriMapper">
        <nav:UriMapping MappedUri="<strong>/ui/SettingsPage.xaml?pvt=Main</strong>" Uri="/Settings" />
    </nav:UriMapper>
</Application.Resources>

Obviously, the MappedUri property is what you would normally use in conjunction with NavigationService.Navigate to actually perform the navigation. The Uri property is the new “simplified” Uri that you can use throughout your app to refer to the same page.

If you don’t already have it in your App.xaml file, be sure and add the namespace line for nav:

1
xmlns:nav="clr-namespace:System.Windows.Navigation;assembly=Microsoft.Phone"

Create the New ApplicationBarIconNavigator control

Create a new class, call it ApplicationBarIconNavigator, and use the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
''' <summary>
''' Provides a handy extension to the ApplicationBarIconButton to facilitate direct navigation
''' </summary>
''' <remarks></remarks>
Public Class ApplicationBarIconNavigator
    Inherits ApplicationBarIconButton
 
 
    Public Property URI As Uri
        Get
            Return _Uri
        End Get
        Set(value As Uri)
            _Uri = value
        End Set
    End Property
    Private _Uri As Uri
 
 
    Private Sub ApplicationBarIconNavigator_Click(sender As Object, e As System.EventArgs) Handles MyBase.Click
        DirectCast(App.Current.RootVisual, PhoneApplicationFrame).Navigate(Me.URI)
    End Sub
End Class

This Class inherits from the normal ApplicationBarIconButton, but makes two critical changes.

First, it exposes a URI property, to allow us to specify what URI we want to navigate to.

Second, it automatically handles the Click event, and navigates to that URI.

This combination allows us to specify the URI to navigate to and handle the navigation event, all from XAML with no code on the page’s code behind at all.

The ApplicationBarDefinition

Now, in your page’s xaml, usually toward the end of the file is the definition of the ApplicationBar.

1
2
3
4
5
<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False">
        <shell:ApplicationBarIconButton IconUri="/Images/Settings.png" Text="Settings" Click="Settings_Click"/>
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Note that I’ve got only one button defined, and its Click event is set to About_Click, so normally, I’d have to navigate to the event handler, and add code there to perform the actual navigation:

1
2
3
Private Sub Settings_Click(ByVal sender As Object, ByVal e As EventArgs)
    Me.NavigationService.Navigate(New Uri("<strong>/ui/SettingsPage.xaml?pvt=Main</strong>", UriKind.Relative))
End Sub

Not a huge issue, but it does mean you’ve got code behind where there really doesn’t need to be any, which means more code to maintain, etc.

But, with the new control in place (you may have to perform a Build before you can add a reference on the page to it without error), and with the URIMappings set up, you can now do something like this:

1
2
3
4
5
<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False">
        <local:ApplicationBarIconNavigator <br>                IconUri="/Images/Settings.png" <br>                Text="Settings" <br>                Uri="/Settings" />
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

At this point, you’re done! You no longer need the event handler logic because the ApplicationBarIconNavigator button will automatically handle it. And your URI complexity is isolated to just the App.xaml resources. Sweet!

One final note, if you haven’t already included one, you’ll need to add a “local” namespace declaration in your page’s xaml so that local:ApplicationBarIconNavigator will resolve.

1
xmlns:local="clr-namespace:{your project name}"

Wrapup

With MVVM frameworks like Prism, I believe this kind of functionality is baked in to some degree. But in my case, just starting out with WP7, I believe it’s important to understand the underpinnings of the system first, then progress to a higher level framework.