Referenced Assemblies and XrmToolBox Tools

In my recent post Major update to XrmToolBox Shared Controls, I mentioned an outstanding issue with ILMerge with the shared controls library. I use a sample Tool creatively named Control Tester Tool, for testing that references the Shared Controls. This allows me to test
each user control user interface, including public interfaces, properties, methods, and events. I had hoped that ILMerge would work for myself and other developers rather than needing to deploy the Shared Controls assembly.

Unfortunately, when XrmToolBox attempts to load the Control Tester Tool, I receive a File Not Found exception when CLR fails to resolve the Shared Controls referenced assembly. This is odd because everything should be available with the ILMerge operation. Using ILSpy, I can verify the Shared Control components are present in merged Control Tester Tool assembly.

This seems familiar…

ILMerge is not officially supported by the Dynamics team anymore (check out this article by Scott Durow: If you’re using ILMerge with your plugins – make sure you read this!) but I have used it on another XrmToolBox Tool without issue.

However, I have seen the assembly resolve/File Not Found exception issue with another Tool. I traced that instance down to a public property on my Tool that using a class from a referenced assembly. In that case, I had a public, browsable property on my Tool’s user control class that uses a component from a referenced assembly. Once I made this property internal, my Tool loaded without any exceptions.

I have a similar setup with the Shared Controls assembly. Each ListView style controls in the library has a property named ListViewColumnDef that is decorated with the [Browsable] attribute so that developers can edit this property at design time.


[Category("XrmToolBox")]
[DisplayName("List View Columns")]
[Description("List of Column Definitions for the main ListView.  If not specified, the Columns will be automatically generated using the type of object in the AllItems list")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ListViewColumnDef[] ListViewColDefs {
    ...
}

And the design time property:

When the InitializeComponent method is called in the user control constructor, the Fusion Log errors indicate that the .NET runtime is searching for the original assembly containing this class in the host XrmToolBox application folder.

I believe the assembly resolution error is due to the order in which the XrmToolBox loads each Tool assembly and how it resolve references. The XrmToolBox uses the Managed Extensibility Framework (MEF) to load the Tool assemblies. This may be restricting where and how the runtime resolves referenced assemblies and it’ simply does not recognize components within assemblies combined with ILMerge!

A solution!

I would really like to deploy the Shared Controls assembly as part of the XrmToolBox platform, but that still requires some work and review from the XrmToolBox team. In the meantime, we have an alternate method for loading referenced assemblies when your XrmToolBox Tool constructor fires.

This method first requires including your referenced assembly with the XrmToolBox Tool assembly NuGet package. In Control Tester Tool and Shared Controls example, I would include the xrmtb.XrmToolBox.controls.dll assembly in the NuGet package for the assembly Sample.XrmToolBox.TestPlugin.dll. To avoid any clashes in the Plugin folder, I created a sub folder with the same name as the Control Tester Tool, Sample.XrmToolBox.TestPlugin, included in the NuGet package. When I build my solution with the Release configuration, I copy my Tool assembly into the Plugins folder and references onto the new Sample.XrmToolBox.TestPlugin sub folder. A similar convention should be setup for the Debug configuration so that we can properly debug our Tool.

My Post-Build steps are a bit lengthy but they no longer require the ILMerge step:

IF $(ConfigurationName) == Debug (
IF NOT EXIST Plugins mkdir Plugins
IF NOT EXIST Plugins\$(TargetName) mkdir Plugins\$(TargetName)
xcopy "$(TargetDir)$(TargetFileName)" "$(TargetDir)Plugins\" /Y
xcopy "$(TargetDir)$(TargetName).pdb" "$(TargetDir)Plugins\" /Y
xcopy "$(TargetDir)xrmtb.XrmToolBox.Controls.*" "$(TargetDir)Plugins\$(TargetName)" /Y
)
REM for NuGet package
IF $(ConfigurationName) == Release (
IF NOT EXIST $(ProjectDir)NuGet\Package\lib\net47\Plugins\ mkdir $(ProjectDir)NuGet\Package\lib\net47\Plugins\
IF NOT EXIST $(ProjectDir)NuGet\Package\lib\net47\Plugins\$(TargetName)\ mkdir $(ProjectDir)NuGet\Package\lib\net47\Plugins\$(TargetName)\
xcopy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)NuGet\Package\lib\net47\Plugins\" /Y
xcopy "$(TargetDir)xrmtb.XrmToolBox.Controls.dll" "$(ProjectDir)NuGet\Package\lib\net47\Plugins\$(TargetName)\" /Y
)

If you would like to avoid the Post-Build events , you can also leverage Azure DevOps to automate this build process as described by Jonas Rapp in his article Use Azure DevOps to publish XrmToolBox plugins.

Using AssemblyResolve

This article describes the proposed solution, specifically the section describing the AssemblyResolve event on the AppDomain object: How to load an assembly at runtime that is located in a folder that is not the bin folder of the application.

The AssemblyResolve event “fires whenever the common language runtime tries to bind to an assembly and fails.” Attaching to this event will allow us to load any referenced assemblies for our XrmToolBox Tool that the CLR fails to resolve.

Wire up the event

I’ve only made a few modifications to the example in the article is pretty clear and for my case. First, we attach to the event in the Tool constructor. In my Control Tester Tool, the parameter-less constructor looks like this:

public class ControlTesterPlugin : PluginBase
{
    public ControlTesterPlugin() {

        // hook into the event that will fire when an Assembly fails to resolve
        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyResolveEventHandler);
    }
    ...
}

The constructor just wires up the event handler and so that, as our Tool is loaded by the XrmToolBox, we have a chance to manually load assemblies that fail to resolve.

Handle the event

Now, we can take a look at our version of the event handler. Most of it remains the same as the article, but we’ve made some specific updates.

private Assembly AssemblyResolveEventHandler(object sender, ResolveEventArgs args)
{
    Assembly loadAssembly = null;
    Assembly currAssembly = Assembly.GetExecutingAssembly();
            
    // base name of the assembly that failed to resolve
    var argName = args.Name.Substring(0, args.Name.IndexOf(","));

    // check to see if the failing assembly is one that we reference.
    List refAssemblies = currAssembly.GetReferencedAssemblies().ToList();
    var refAssembly = refAssemblies.Where(a => a.Name == argName).FirstOrDefault();

    // if the current unresolved assembly is referenced by our plugin, attempt to load
    if (refAssembly != null)
    {
        // load from the path to this plugin assembly, not host executable
        string dir = Path.GetDirectoryName(currAssembly.Location).ToLower();
        string folder = Path.GetFileNameWithoutExtension(currAssembly.Location);
        dir = Path.Combine(dir, folder);

        var assmbPath = Path.Combine(dir, $"{argName}.dll");

        if (File.Exists(assmbPath))
        {
            loadAssembly = Assembly.LoadFrom(assmbPath);
        }
        else {
            throw new FileNotFoundException($"Unable to locate dependency: {assmbPath}");
        }
    }

    return loadAssembly;
}

  

The ResolveEventArgs args argument provides the details on the assembly that failed to load and triggered the event. We can check this argument and verify whether it’s an assembly we care to load. Otherwise, we return null and let the CLR handle the assembly resolution normally.

Our updated event handler will first get the name of the assembly that failed to load using the args.Name property. The Shared Controls assembly would have the name xrmtb.XrmToolBox.Controls. We could stop here and hard code this referenced assembly name, but what if we later add another referenced assembly that requires a manual load?

Instead we check to see if the assembly failing to load is in the list assemblies that our Tool references. This is available in the assembly metadata via the call to GetReferencedAssemblies(). If the assembly failing to load is in our Tool’s list of references, we should load it manually.

Finding the referenced assembly

Loading the assembly is then relatively straightforward using the static method Assembly.LoadFrom() method. The only quirk that we want to be sure to load the referenced assembly from the correct location. I suggest that the assembly be loaded from a sub-folder of the Plugins folder with the same name of my Control Tester Tool. I use this approach because the XrmToolBox offers the ability to change the location of your Plugins folder through the /overridefolder command line option.

To build the location of our referenced assembly, we first grab the location of the Tool assembly. We then use the name of the Tool assembly for the sub folder, following the convention we used when building the NuGet package structure. When my Tool is installed, the referenced assembly should be included no matter where the Plugin folder is configured.

In our example, we have a XrmToolBox Tool with the assembly name of Sample.XrmToolBox.TestPlugin.dll. The NuGet Package includes the assembly and a sub-folder named Sample.XrmToolBox.TestPlugin in which we included the referenced assembly xrmtb.XrmToolBox.Controls.dll.

If I happen to add another referenced assembly in the future, I should be able to simply add the new referenced assembly to my sub-folder and it can be loaded within this event handler!

Wrapping up…

I would love to have a much more concrete reason for these issues and I plan on doing more investigation. In the meantime, I think this is a relatively simple and low risk method for deploying and loading referenced assemblies with our XrmToolBox Tools. It is working so far in my situation with a shared assembly that I created, but I plan on doing more testing to head off any potential issues that I have not run into so far.

This is just one proposed solution to get around some issues with loading referenced assemblies for our XrmToolBox Tools. I would definitely like to alternative methods to tackling the issue or any problems that you may find with this proposed solution. As always, comments, corrections, and suggestions are always welcome!

0 Points