I’m completely onboard the not mixing AppDomains train. I only even started learning about them because of the behavior documented above. I would love to use DI for all of this. What are your thoughts on setting up unit tests for retrieving custom UserData from a RhinoObject?
For reference, my current approach follows this pattern. In a class library define an Interface as well as data retrieval callback delegates. Then implement that interface with custom userdata in the plugin and assign the callback functions for adding the data and retrieving the data during plugin startup. It all looks something like this:
IChimeraUserData.cs
namespace Chimera.Shared;
interface IChimeraUserData
{
string Data {get; set;}
}
static class UserDataHelpers
{
public delegate IChimeraRhinoData CreateUserObjectDelegate(RhinoObject ro);
public delegate bool RemoveUserObjectDelegate(RhinoObject ro);
public delegate bool TryGetUserObjectDelegate(ObjectAttributes objectAttributes, out IChimeraRhinoData rhinoData);
public static CreateUserObjectDelegate CreateObjectFunc { get; set; }
public static RemoveUserObjectDelegate RemoveObjectFunc { get; set; }
public static TryGetUserObjectDelegate TryGetObjectFunc { get; set; }
}
PluginRhino.cs
namespace Chimera.PluginRhino;
public class PluginRhino : Rhino.PlugIns.PlugIn
{
protected override LoadReturnCode OnLoad(ref string errorMessage)
{
UserDataHelpers.CreateObjectFunc = ChimeraData.CreateObject;
UserDataHelpers.RemoveObjectFunc = ChimeraData.RemoveObject;
UserDataHelpers.TryGetObjectFunc = ChimeraData.TryGetObject;
return base.OnLoad(ref errorMessage);
}
}
ChimeraData.cs
namespace Chimera.RhinoPlugin;
[Guid("00000000-0000-0000-0000-000000000000")] //blanked in this example
public class ChimeraData : Rhino.DocObjects.Custom.UserData, IChimeraUserData
{
public ChimeraData()
{
}
public override string Description => "Custom User Data for Chimera";
public string Data {get; set;}
protected override void OnDuplicate(UserData source)
{
base.OnDuplicate(source);
if(source is not ChimeraData sourceData) return;
Data = sourceData.Data;
}
#region Serialization
private const int _MAJOR_VERSION = 0;
private const int _MINOR_VERSION = 1;
public override bool ShouldWrite => true;
protected override bool Read(BinaryArchiveReader archive)
{
archive.Read3dmChunkVersion(out int major, out int minor);
Data = archive.ReadString();
return true;
}
protected override bool Write(BinaryArchiveWriter archive)
{
archive.Write3dmChunkVersion(_MAJOR_VERSION,_MINOR_VERSION);
archive.WriteString(_data);
return true;
}
#endregion
#region UserData Callback Implementations
public static IChimeraRhinoData CreateObject(RhinoObject ro)
{
if (TryGetReferenceObject(ro.Attributes, out var existingRef)) return existingRef;
var roAtt = ro.Attributes.Duplicate();
var userObject = new ChimeraData{ Data = "Initial Data"};
roAtt.UserData.Add(userObject);
ro.Document.Objects.ModifyAttributes(ro, roAtt, true);
return userObject;
}
public static bool TryGetObject(ObjectAttributes attributes, out IChimeraRhinoData rhinoData)
{
var cdata = attributes.UserData.Find(typeof(ChimeraData));
rhinoData = (IChimeraRhinoData)cdata;
return rhinoData is not null;
}
public static bool RemoveObject(RhinoObject ro)
{
var roAtt = ro.Attributes.Duplicate();
var userObject = roAtt.UserData.Find(typeof(ChimeraData));
if (userObject is null) return false;
if (!roAtt.UserData.Remove(userObject)) return false;
ro.Document.Objects.ModifyAttributes(ro, roAtt, true);
return true;
}
#endregion
}
This all works and makes it easy to get user data in context of the shared library, the rhino plugin, or the associated grasshopper plugin. I’ve tested via normal debugging without issue. But creating unit tests for it has proven difficult, and is where I ran into the issue with Rhino.Inside, and Rhino in general, always running on the default application domain. With the unit test runners running the tests in a subdomain, and the plugin in being loaded an run on the default domain, the userdata is null. When I attach a debugger, I can see the userdata being added to the object, see it there in the context of the plugin, and then when I inspect the object in the context of the test, the userData on the object attributes has a null in the place where the userdata should be. In fact, because the callbacks are set as part of the plugin load function, they’re null when I try and use them in the context of the tests. This is where I got to about a month ago, and where I went looking for a way to handle dealing with cross-appdomain things.