What Is Different About How The Add-In Manager Loads DLLs

What Is Different About How The Add-In Manager Loads DLLs

mastjaso
Advocate Advocate
2,919 Views
12 Replies
Message 1 of 13

What Is Different About How The Add-In Manager Loads DLLs

mastjaso
Advocate
Advocate

I've presently got an issue related to a third party WPF controls library. I've gone through and developed 95% of my software using the Add-In Manager and everything has been working flawlessly, however, now that I'm heading into the finish line and added in Ribbon buttons and tried testing by installing it, I'm getting DLL errors. More specifically I'm getting an error that it can't find or load my third party controls assembly if I try and use the third party gridview, despite the fact that the dll is in the proper addins folder, and everything works flawlessly if loaded through the add in manager.

So I'm wondering, what is different about how the add in manager loads in dlls vs how Revit does it at startup that could be causing this difference in behaviour?

0 Likes
2,920 Views
12 Replies
Replies (12)
Message 2 of 13

jeremytammik
Autodesk
Autodesk

Dear Jason,

 

Thank you for your query.

 

Sorry to hear about the problem you ran into.

 

I have no idea what the difference might be, so I cannot help you with that question.

 

However, it may not be to the point.

 

On the question how you can successfully load your add-in and the support DLLs it requires, I do have a suggestion or two to make:

 

1. Install the add-in in or below the directory containing Revit.exe. This might make .NET happy.

 

2. Use a custom assembly resolver:

 

http://thebuildingcoder.typepad.com/blog/2014/05/rvtva3c-assembly-resolver.html

 

I hope this helps.

 

Best regards,

 

Jeremy

 



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

0 Likes
Message 3 of 13

mastjaso
Advocate
Advocate

 

Hi, thank you for the information and I was actually given the same suggestion (re: putting it in the same folder as revit.exe) from the control maker's support team but that didn't seem to help. Their team also suggested adding those support assemblies to the GAC using gacutil.exe and it would allow it function, but that's not ideal for a variety of reasons including needing admin rights.

 

But what I ended up figuring out is that I believe this is a bug with the way Revit finds DLLs to load into its AppDomain, where it ignores assemblies that are only referenced in XAML. For example I was using a gridview control called ThirdPartyGridView that came in a separate ThirdParty.Controls.GridView.DLL. Because I was using MVVM, this GridView namespace contained within that DLL was never explicitly referenced anywhere in my C# "code behind" or viewmodel. It appears that if that's the case Revit does not seem to know that it needs to load in the DLL that holds the GridView control / namespace. Once I added in an otherwise useless statement that explicitly referenced that namespace in C# such as:

 

string x1 = ThirdParty.Controls.GridView.RandomEnum.ToString();

Revit started loading the GridView DLLs into its AppDomain properly. I believe this is a bug because in an ideal MVVM program you'd *never* reference your controls classes / namespaces from the code behind, just in XAML, so there shouldn't be a need for this. Also because the Add In Manager doesn't have this problem and can find all the dependent assemblies regardless of whether they're explicitly referenced in XAML or C#.

0 Likes
Message 4 of 13

Anonymous
Not applicable

Hi Jeremy,

 

It seems that I am stuck in the same problem. I'm using a third party lib called Caliburn.Micro in my addin. And it runs well when I called it in the Addin Manager. But, when I run it from the Ribbon Button, it always shows the same error of FileNotFoundExecption.

 

Here is the error information:

System.IO.FileNotFoundExecption:: Could not load the file or the assembly 'Caliburn.Micro.Platform, PublicKeyToken=8e5891231f2ed21f' . The system cannot find the file specified.

 

Caliburn.Micro is a lib for MVVM framework in WPF. I have tried to move all my DLLs in the subfolder Addin of the Revit.exe directory. It doesn't work. And then I thought maybe it's a conflict of version between the one Revit using and the one I got. But with a search in the folder. I have not found the Caliburn.Micro.dll in Revit. The second suggestion with this post in your blog, for me it's seems to reference the DLL that Revit is using. Since Revit is not using the Caliburn.Micro. So, it will not work for me either.

 

I have done some research for that, and for the suggestion of @mastjaso , since Caliburn.Micro is a lib for the framework. So, I have already directly referenced it in a lot of place in my code-behand.

 

I'm run out of idea now, hope you can give me some help here.
// Using Revit 2019, Net4.7, Caliburn.Micro v3.2.0

 

Best regard,

 

Yousheng

0 Likes
Message 5 of 13

jeremytammik
Autodesk
Autodesk

Dear Yousheng,

 

Thank you for your query.

 

I believe the Add-In Manager reads the assembly DLL into memory and then loads the actual .NET executable from a byte stream from memory instead of reading the actual physical file.

 

You might try implementing the same approach for your add-in yourself.

 

I hope this helps.

 

Best regards,

 

Jeremy

 



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

Message 6 of 13

awmcc90VZTT2
Contributor
Contributor

This is actually a very common issue that most people run into as they expand their projects and start to incorporate 3rd party libraries. Don't get me started on resolving the correct version of Newtonsoft.. but I digress.

 

I'd recommend installing a tool called Process Monitor which will allow you to see where Revit is attempting to load the libraries from. Generally looks in the same root places like the ProgramFiles\Autodesk\Revit <Version>\ directory, all GAC locations, and then differs depending on how the addin was installed.

 

The best way I've found to ensure that all my libraries get loaded correctly, targeting the correct version, is to create a bundle for the addin. Here is the example provided by Autodesk. Here is a bundle that I put together for a Navisworks addin (same format, differing targets in the package contents). Put all your libraries in the same directory that your addin is loaded from and all will resolve correctly. 

 

I'm almost 100% sure that the same is true if you put your ".addin" file in the C:\Users\<user>\AppData\Roaming\Autodesk\Revit\Addins\<year>\ directory and then make the Assembly attribute \<your addin>\addin.dll and then place all your dependencies in that sub directory as well then they will load correctly but I'd have to double check that.

 

 

Message 7 of 13

Anonymous
Not applicable

Hello again, Jeremy.
 
Sorry for the late, I have been on vacation for the past two weeks. I have picked up this question again later this week.
 
What I have tried so far:
 
1. Create a bundle for the addin.
I have correctly put all my lib in the content folder and write the PackageContent.xml and the .addin file, but eventually, it failed to resolve the assembly.
 
2. Put the ".addin" file in "...Autodesk\Revit\Addins\2019\" folder.
And placed all dlls in that subdirectory, then make the addin file target the path of that subdirectory. It's failed too.
  
The interesting thing is that I run my command from the Addin Manager just one time, after that everything runs well magically!
 
With the tool "Process Monitor" that @awmcc90VZTT2 suggested, I found the Addin Manager loads all dlls relative whatever which command you have run the first time and all assembly resolves correctly.
 
Then, I came back to your solution of read the assembly file in a Stream and load from memory.

With lots of test, it works!

 

I create a class called ExternalAssemblyResolver. Here is my code.

 

    internal class ExternalAssemblyResolver
    {
        public static void Register(string assemblyName)
        {
            AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
            {
                var an = new AssemblyName(args.Name);
                if (an.Name == assemblyName)
                {
                    // Load external assembly path, remind put the 3rd party assembly in the same folder as your addin assembly.
                    string assemblyPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), assemblyName + ".dll");
                    using (Stream stream = new System.IO.MemoryStream())
                    {
                        if (File.Exists(assemblyPath))
                        {
                            byte[] data = File.ReadAllBytes(assemblyPath);
                            stream.Read(data, 0, data.Length);
                            return Assembly.Load(data);
                        }
                    }
                }
                return null;
            };
        }
    }

 

Then, just add this line in the addin OnStartup function, it's done.

ExternalAssemblyResolver.Register("YourAssemblyNameWithoutExtension");

 

I think this will be the most easy way to load some external assembly for now.

Thanks a lot, and extremely grateful @awmcc90VZTT2 for all your suggestions .
 
Best regards, 
 
Yousheng.

0 Likes
Message 8 of 13

awmcc90VZTT2
Contributor
Contributor

Glad you were able to resolve the issue. One point of caution with your solution: you may inadvertently cause other addins and revit functionality to fail when hooking the assembly resolve event. 

 

The app domain is the revit app domain, thus if other addins use the same library you are trying to resolve then you can cause them to load an incorrect version. I have used this method in the past and had this issue. I added a static flag to my addin commands to ensure that my commands were the active at the time of the assembly resolution so that I didn't affect the functionality of anything other than my stuff.

 

You may need to implement something similar if you or anyone using your custom tools has this problem. 

0 Likes
Message 9 of 13

Anonymous
Not applicable

Thanks Andrew, you are totally right. 

 

But, I don't have much experience in this area.

I just making the ExternalAssemblyResolver class static and internal to prevent access to others.

Do you think this is enough? Or I need do something else?

 

Thanks again.

 

 

0 Likes
Message 10 of 13

awmcc90VZTT2
Contributor
Contributor

With this route what I normally do is create a "Session" class and have that be a singleton or static class that handles all the session related information. In this case it would just be whether the addin is active or not.

 

public static class Session
{
    public static bool IsActive = false;
}

 

Then in my commands I would wrap them like this:

 

public class CustomAddinCommand : IExternalCommand
{
    public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
    {
        try 
        {
            Session.IsActive = true;
            ...
            return Result.Succeeded;
        }
        finally
        {
            Session.IsActive = false;
        }
    }
}

 

And then in my assembly resolve:

 

private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    // Make sure our product is the one being used
    if (Session.IsActive && <is_assembly_we_want_to_resolve>)
    {
        ...
        return <resolved_assembly>;
    }
    return null;
}

 

As far as declaring them static and internal, that doesn't really matter in terms of access from others given all addins are loaded and trusted the same amount in the Revit AppDomain. Any other addin could potentially hook and use reflection to access those members. That's not something you should worry about as it will never happen.

 

If you follow the steps above then you should be able to ensure that your commands are executing at the time of resolution. In the off chance you need to do some operation at the startup time of your addin then you can either set your session to active for that operation or delay execution until one of your commands is executed.

Message 11 of 13

Anonymous
Not applicable

I got it, it's great to avoid some potential risks.

Very grateful for all your guide, Andrew

 

Yousheng.

0 Likes
Message 12 of 13

tu.phan
Explorer
Explorer

Hi @Anonymous ,

 

I tried using Caliburn Micro with my Inventor Add In recently and tried implementing your ExternalAssemblyResolver to resolve Caliburn.Micro.dll but it still doesn't seem to be working.

 

I added this code in the Inventor Add-In's Activate() method:

 

ExternalAssemblyResolver.Register("Caliburn.Micro");

 

Were there any other assemblies you had to resolve to get it to work? My Add In is able to load, but the View is not able to bind with the ViewModel. I only have one property in the ViewModel:

 

public string TestNumber { get; set; } = "123";

 

And I'm trying to bind it to a Label in the View.xaml using x:Name. The view is able to be opened, but the "123"string is not displaying.

 

        <Label x:Name="TestNumber"
               Grid.Column="1" 
               Grid.Row="1"/>

 

0 Likes
Message 13 of 13

csvensrudMarchConsult
Participant
Participant

Did anyone get Caliburn Micro working properly I am struggling to get the Configure event to fire. Trying to ste up containers for services.

0 Likes