Unable to get resource DLLs to load for use in executable

Tool: Visual Studio Code, PowerShell Module
Version: PS Pro Tools v2022.7.1, VSCode v1.70.0

Hello, I am trying to load a dll file from a resource into my code for use as an executable. I am having some issues with it working and was hoping I could get some assistance in pointing me in the right direction.

Below is the code I am using:

function Get-Resource {
     param($Name)
     
     $ProcessName = (Get-Process -Id $PID).Name
     $Stream = [System.Reflection.Assembly]::GetEntryAssembly().GetManifestResourceStream("$ProcessName.g.resources")
     $KV = [System.Resources.ResourceReader]::new($Stream) | Where-Object Key -EQ $Name
     [System.IO.StreamReader]::new($KV.Value).ReadToEnd()
}

[byte[]]$MahAppsDll = Get-Resource -Name "MahApps.Metro.dll"
[System.Reflection.Assembly]::Load($MahAppsDll)

I am getting a bunch of non-sense when running the exe but at the very start of the non-sense is “Cannot convert value” and “This program cannot be run in DOS mode.”

Any assistance would be greatly appreciated.

StreamReader.ReadToEnd is returning the DLL as a string and then you are trying to cast it to a byte array and that’s causing the issue.

Try this:

function Get-Resource {
     param($Name)
     
     $ProcessName = (Get-Process -Id $PID).Name
     $Stream = [System.Reflection.Assembly]::GetEntryAssembly().GetManifestResourceStream("$ProcessName.g.resources")
     $KV = [System.Resources.ResourceReader]::new($Stream) | Where-Object Key -EQ $Name
     [System.IO.BinaryReader]::new($KV.Value).ReadBytes($KV.Value.Length)
}

[byte[]]$MahAppsDll = Get-Resource -Name "MahApps.Metro.dll"
[System.Reflection.Assembly]::Load($MahAppsDll)

Thank you, that helped in getting the DLL and dependencies loaded; I realized that that method wouldn’t work and found a roundabout way to do the same thing but this method is much better.

However I’ve run into a new issue that only seems to be occurring when compiling to EXE. Not sure that it’s really PSProTools related, but figured I might as well ask if you have any suggestions (code is below). Essentially, If I just LoadWithPartialName the MahApps.Metro.dll and ControlzEx.dll files and skip the resource stuff, the GUI loads fine. But if I comment out the LoadWithPartialName lines and set up all the resource options (as outlined in the below code), I get an exception on the Load line (error in code block below my code).

[System.Reflection.Assembly]::LoadWithPartialName("PresentationFramework") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null

function Import-Xaml {
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        $Xaml
    )

    $manager = New-Object System.Xml.XmlNamespaceManager -ArgumentList $Xaml.NameTable
    $manager.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml")
    $xamlReader = New-Object System.Xml.XmlNodeReader $Xaml
    [Windows.Markup.XamlReader]::Load($xamlReader)
}

function Get-Resource {
    param($Name)
    
    $ProcessName = (Get-Process -Id $PID).Name
    $Stream = [System.Reflection.Assembly]::GetEntryAssembly().GetManifestResourceStream("$ProcessName.g.resources")
    $KV = [System.Resources.ResourceReader]::new($Stream) | Where-Object { $_.Key -EQ $Name }
    [System.IO.StreamReader]::new($KV.Value).ReadToEnd()
}

function Get-BinaryResource {
    param($Name)
    
    $ProcessName = (Get-Process -Id $PID).Name
    $Stream = [System.Reflection.Assembly]::GetEntryAssembly().GetManifestResourceStream("$ProcessName.g.resources")
    $KV = [System.Resources.ResourceReader]::new($Stream) | Where-Object { $_.Key -EQ $Name }
    [System.IO.BinaryReader]::new($KV.Value).ReadBytes($KV.Value.Length)
}

$global:SYNC = [Hashtable]::Synchronized(@{})
[xml]$xamlFile = Get-Resource -Name "jtools.xaml"

[byte[]]$ControlzEx = Get-BinaryResource -Name "ControlzEx.dll"
[byte[]]$MahApps = Get-BinaryResource -Name "MahApps.Metro.dll"

[System.Reflection.Assembly]::Load($ControlzEx)
[System.Reflection.Assembly]::Load($MahApps) 

$MainWindow = Import-Xaml -Xaml $xamlFile

$MainWindow.ShowDialog()
Exception calling "Load" with "1" argument(s): "The invocation of the constructor on type 'MahApps.Metro.Controls.MetroWindow' that matches the specified binding constraints threw an
exception."
At line:17 char:5
+     [Windows.Markup.XamlReader]::Load($xamlReader)
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : XamlParseException

You cannot call a method on a null-valued expression.
At line:145 char:1
+ $SYNC.MainWindow.ShowDialog()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

I think that the real error message is getting wrapped. Try this:

try {
[Windows.Markup.XamlReader]::Load($xamlReader)
} catch {
 $_.Exception.InnerException
}

Unfortunately it came back with the same message:

The invocation of the constructor on type 'MahApps.Metro.Controls.MetroWindow' that matches the specified binding constraints threw an exception.

You might need to dig in a couple exceptions deep.

try {
[Windows.Markup.XamlReader]::Load($xamlReader)
} catch {
    $e = $_.Exception.InnerException
    while ($e.InnerException -ne $null)
         $e = $e.InnerException 
    }
    $e
}

Ah, yes that produced a bit more information, but a bit confusing since I am loading the ControlzEx assembly as shown in my code snip in my original post. I have even included “Microsoft.Xaml.Behaviors.dll” as per the “Dependencies” page on the NuGet website for .NET 4.6.2 even though I hadn’t referenced it by name in the script previously.

Could not load file or assembly 'ControlzEx, Version=4.0.0.0, Culture=neutral, PublicKeyToken=69f1c32f803d307e' or one
of its dependencies. The system cannot find the file specified.

I made the script output the full details of loading the dlls and I noticed that the “FullName” listed is FullName : ControlzEx, Version=5.0.0.0, Culture=neutral, PublicKeyToken=69f1c32f803d307e and does not match what’s being looked for, when compiling to an EXE (Version=4.0.0.0). I downloaded the 4.4.0 version of ControlzEx and loaded that one instead, and it worked just fine.

Very weird that it works with the 5.0.1 assembly when run as a ps1 but once compiled to an exe (even referencing the assemblies with the “LoadFrom” file option, rather than the resources) it was looking specifically for the 4.x version of the assembly.

So, I found how to get around the requirement of the old version was to use a bindingRedirect in the filename.exe.config after packaging the script as an EXE (see below). FYI, this is very case sensitive for anyone that may come across this as an answer to their problem(s) in the future; I tried to use “PublicKeyToken” instead of “publicKeyToken” and the binding did not take effect until using the proper case on the “p”.

Side Effect: using this method means that if you try to change to the 4.x version, you will need to remove this code as it is explicitly forcing all references to 4.0.0.0 to redirect to 5.0.0.0

My question for you @adam is: Is it possible to keep this config file from being overwritten every time I package my script? Or to somehow inject this runtime section into the config file during/after packaging?

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="ControlzEx" publicKeyToken="69f1c32f803d307e" culture="neutral" />
        <bindingRedirect oldVersion="4.0.0.0" newVersion="5.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

We currently don’t have a mechanism for this. It’s something we could add to our packaging process.