Referenced dll’s without locking in 3dsMax

This post is about using .NET dll’s in 3dsMax. In particular using assemblies which in turn reference other assemblies. This happens for instance when I write a wrapper for another library. In this example it’s about json.net by Newtonsoft.

There are several ways to load assemblies in 3dsMax. I cover loading an assembly by bytes. There are advantages to this method. I can write an assembly in Visual Studio and by loading the bytes I don’t lock the assembly. This means I can work on the assembly in Visual Studio and my script in 3dsMax side-by-side without having to restart 3dsMax when making updates in Visual Studio.

--Loading a dll by bytes in 3dsMax
local assemblyBytes = (dotnetClass "System.IO.File").ReadAllBytes @"path\to\assembly.dll"
local myAssembly = (dotnetClass "System.Reflection.Assembly").Load assemblyBytes

A disadvantage however is this assembly can’t access another assembly, even if I’ve set up the references in Visual Studio correctly. It will work in Visual Studio but it won’t in 3dsMax. Bummer! This article covers a workaround albeit a rather laborious one.

Example project

Regularly I need to read or write json from within 3dsMax to talk to an API. A library called json.net makes it easy to read and write json and convert my json to regular C# classes. To use json.net in Visual Studio I reference it in my project and I’m ready to go.

A sample json file with an array of json objects
A sample json file with an array of json objects

Let’s set up a project in Visual Studio without looking at 3dsMax. I’ve created a class library which reads a json string from a file and parses it into a list of objects. I’ve also created a class to match the structure of the json. I did that here: http://json2csharp.com. It saved me some typing. Then I’ve included a method to convert the array of json objects to a list of .NET objects. I use a few methods and objects supplied by json.net. Finally I’ve written a small console app to test this out: load json and parse it. This works great! So what’s the fuss?

//converts a jsonstring which contains an array of objects to a list of jsonColor objects
public IList ReadColors(string filePath)
{
    //get the string contents of the file
    string jsonString = File.ReadAllText(filePath);

    JArray array = JArray.Parse(jsonString);
    IList colorList = new List();
    foreach (JToken token in array)
    {
        colorList.Add(token.ToObject());
    }
    return colorList;
}

Use the dll in 3dsMax

Now that it works in Visual Studio I can make it work in 3dsMax as well. First I create a script to replace the console app. I’ve already shown how to load the assembly. I add the method to convert the json to a list of objects and I add a loop to print out those objects. However, this script fails. The error message is a bit vague but to my understanding it’s the cause is my dll can’t locate the json.net dll.

Here’s the entire script:

--first load the assembly
local assemblyBytes = (dotnetClass "System.IO.File").ReadAllBytes @"path\to\assembly.dll"
local myAssembly = (dotnetClass "System.Reflection.Assembly").Load assemblyBytes
--get the JsonReader class and create an instance of it
local JsonReaderClass = myAssembly.GetType("MyLibrary.JsonReader")
local JsonReader = (dotNetClass "System.Activator").CreateInstance JsonReaderClass
--call the method to parse the json string
local jsonPath = @"path\to\json.json"
local colorList = JsonReader.ReadColors jsonPath
--print the result
for n = 0 to colorList.count-1 do
(
    format "%: %\n" colorList.item[n].color colorList.item[n].value
) 

Simulate failure

First I’ll set up my project in Visual Studio to behave the same as 3dsMax. I changed the “Copy Local” property of my json.net reference to “False”. This will stop copying the actual json.net dll to my output directory. I cleaned up the output directory and pressed F5 to run the project and voila! Visual Studio now also can’t find the json.net assembly. To fix this, I have to somehow include the json.net dll in my own dll.

Stop copying the json.net dll. It won't work in 3dsMax
Stop copying the json.net dll. It won’t work in 3dsMax

Including the assembly

To include the json.net dll I just added it as an existing item to my solution. Right click my class library in the solution explorer, Add, Existing item… Find the json.net dll and add it. It will show up in the solution explorer. Select the dll, in the properties panel set the Build action to Embedded Resource. This will embed this dll into my assembly.

Adding the json.net dll to my project
Adding the json.net dll to my project
Embed the json.net dll in my dll
Embed the json.net dll in my dll

More about this here: https://support.microsoft.com/en-us/kb/319292, http://stackoverflow.com/questions/8851991/unraveling-the-confusion-about-embedded-resources, http://www.codeproject.com/Articles/528178/Load-DLL-From-Embedded-Resource

Resolve the assembly

My assembly is now considerable bigger. That’s because I’ve embedded the json.net dll. However running the app still produces an error. Json.net still can’t be found. Since the json.net dll isn’t copied when I build the solution I need to specify how to get to the embedded dll. At least, that’s my understanding of how this works.

There’s an event for that called AssemblyResolve. This event is fired when the app can’t find an assembly it needs. The event handler contains the code to get the embedded resource as a stream, converts it to bytes and loads it as the assembly we need. Of course, I didn’t come up with this myself, but I’ve found this solution here. Keep in mind that the assembly is loaded by name. This name obviously depends on the name of the assembly but also on the name of my namespace. I just added the method to my existing class. The eventhandler has to be added when the class is instantiated.

static JsonReader()
{
	AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyResolver);
}

static Assembly AssemblyResolver(object sender, ResolveEventArgs args)
{
	Assembly thisAssembly = Assembly.GetExecutingAssembly();

	//this is the resource name of the assembly I want to load
	string resourceName = "MyLibrary.Newtonsoft.Json.dll";
	Stream resourceStream = thisAssembly.GetManifestResourceStream(resourceName);

	//convert the stream to something we can load as an assembly
	byte[] buffer = new byte[resourceStream.Length];
	resourceStream.Read(buffer, 0, buffer.Length);
	Assembly referencedAssembly = Assembly.Load(buffer);
	return referencedAssembly;
}

Wrapping up in 3dsMax

The project now runs in Visual Studio. To make it work in 3dsMax I don’t need to change my existing maxscript code. The assembly is resolved in my dll.

Why?

That’s a good question. It’s a lot of work. Instead of this you can just load the dll’s separately with loadassembly and you get the same results. The big advantage of the approach described here is you can keep developing in Visual Studio, update your dll’s and run the code in 3dsMax without having to restart 3dsMax. For instance if I decide to change the structure of the json while developing, I can just do that, compile my dll and continue working in 3dsMax.

I hope this approach makes sense and helps you develop a bit quicker with multiple assemblies.

 

Leave a Reply

* Will not be published