Is it possible to serialize my own classes when saving a rhino document?
Previously I have used user data to save rhino data (point3ds, curves, surfaces etc.) which mirrors my classes - by knowing what I put in I can reconstruct my classes when I get that user data out.
However now my classes are more complex, so I am saving them to my own file types using binary serialization. Is it possible to save my classes to the rhino file, and is there a best practice for doing this?
If it’s not possible to save my classes into the rhino document, presumably I could write something with OpenNURBS to save the rhino/3dm data to my own file types?
Yes, you can save your own class data in 3DM files. You can save them on objects as object user data or in the document as document user data. Plug-ins do this all the time.
As far as best practices, I’m sure you will get a lot of opinions. Personally I like to create object that serialize themselves. For example, if I were to create my own point class, I might add Read() and Write() that looked like this:
bool Read(Rhino.FileIO.BinaryArchiveReader archive)
{
bool rc = false;
if (null != archive)
{
int major, minor;
archive.Read3dmChunkVersion(out major, out minor);
if (1 == major && 0 == minor) // version 1.0 of our point
{
m_x = archive.ReadDouble();
m_y = archive.ReadDouble();
m_z = archive.ReadDouble();
rc = archive.ReadErrorOccured;
}
}
return rc;
}
bool Write(Rhino.FileIO.BinaryArchiveWriter archive)
{
bool rc = false;
if (null != archive)
{
archive.Write3dmChunkVersion(1, 0); // version 1.0 of our point
archive.WriteDouble(m_x);
archive.WriteDouble(m_y);
archive.WriteDouble(m_z);
rc = archive.WriteErrorOccured;
}
return rc;
}
Also, Microsoft put a lot off effort into adding serialization support to .NET classes. For more information on this, I’d start here:
Thanks for your reply - apologies I’ve taken some time to respond.
Your method of self-serializing objects looks quite elegant, however, I think it might be difficult to implement now as my classes are quite large. Regarding the XML route, I’ll hunt around the MSDN stuff for tips, but do you have any samples of writing to the 3DM file as text or as a binary stream?
Actually, this model works quite good for large classes. The key to make each custom object capable of serializing itself. Trust me, Rhino has some complicated classes, and they all serialize in this manner.
I don’t have any samples to point you at. but the Internet is full of them. Let me know if you cannot find what you need.
Thanks again. I guess difficult to implement wasn’t quite what I meant, more that it will be time consuming to write the functions for all my classes. Had I done that as I was building the classes however, it does sound like that would have been the best solution.
Re: Binary Serialization, I used the example at the bottom of the following article last time to create Serialize & DeSerialize functions for my classes, saving them with a custom file extension, which took very little code.
What I’m struggling to see is how to use the FileStream correctly to serialize classes to the .3dm file of the current model. I’ve sort of got it to work with a dodgy use of the Filestream… by using FileMode.Append, I appended my byte stream to the end of the .3dm file, and could read it back out again.
I encountered some issues with this:
The read function needs to know where to start, which means passing a long to FileStream.Position for the reader
I can’t modify the rhino file when it’s open (so I copied it and add the data, mostly as a workaround to test what would happen by doing it this way)
If I save some custom data in the above way, then edit the file by say, drawing a line, rhino can’t save the backup file, giving error code 0, presumably because I’ve messed up the location of stuff in the file
I’m fairly confident it’s not a good way to do it and can foresee quite a few problems with it. I’m presuming this is mostly a .NET problem, e.g. I’m not missing something in the SDK that will help with this?
You don’t want to use a FileStream because you want to serialize to the 3DM file. Use a MemoryStream. Then, you can archive the bytes using RhinoCommon’s BinaryArchiveWriter and BinaryArchiveReader objects.
I haven’t tested this, but it might work:
[Serializable]
class StuartData
{
/// <summary>
/// Members
/// </summary>
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
/// <summary>
/// Constructor
/// </summary>
public StuartData()
{
}
/// <summary>
/// Constructor
/// </summary>
public StuartData(StuartData src)
{
this.X = src.X;
this.Y = src.Y;
this.Z = src.Z;
}
/// <summary>
/// Create
/// </summary>
public void Create(StuartData src)
{
this.X = src.X;
this.Y = src.Y;
this.Z = src.Z;
}
/// <summary>
/// Write to binary archive
/// </summary>
public bool Write(BinaryArchiveWriter archive)
{
bool rc = false;
if (null != archive)
{
try
{
// Write chunk version
archive.Write3dmChunkVersion(1, 0);
// Write 'this' object
IFormatter formatter = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
formatter.Serialize(stream, this);
stream.Seek(0, 0);
byte[] bytes = stream.ToArray();
archive.WriteByteArray(bytes);
stream.Close();
// Verify writing
rc = archive.WriteErrorOccured;
}
catch
{
// TODO
}
}
return rc;
}
/// <summary>
/// Read from binary archive
/// </summary>
public bool Read(BinaryArchiveReader archive)
{
bool rc = false;
if (null != archive)
{
// Read and verify chunk version
int major, minor;
archive.Read3dmChunkVersion(out major, out minor);
if (1 == major && 0 == minor)
{
try
{
// Read this object
byte[] bytes = archive.ReadByteArray();
MemoryStream stream = new MemoryStream(bytes);
IFormatter formatter = new BinaryFormatter();
StuartData data = formatter.Deserialize(stream) as StuartData;
this.Create(data);
// Verify reading
rc = archive.ReadErrorOccured;
}
catch
{
// TODO
}
}
}
return rc;
}
}
Only amendment I had to make was that the functions read or write should return true if rc is false, rather than returning the value of rc, or rhino brings up a message box with unspecified read or write error occured, and I used a different version-ing. Hope that helps anyone that might find this in the future.
I am trying to use this method to serialize my plugin state. Its sort of working but I had a few questions. I am using the second example above. If I have two objects (different classes), should I create a read/write method in each(similar to first example)? Then I call these individually from the overridden methods in the Plugin? The two classes in my case are a model and a viewmodel. I am hoping to deserialize my UI state. If that doesn’t work I will have to try to rebuild the UI from the model.
I guess my actual question is wrt to the BinaryArchiveReader archive object that is getting passed, does each read and write method just “magically” pull the relevant data from this? How does it not overwrite the other object?
On second thought, my ViewModel contains a reference to the Model, so I am going to try to let it serialize via that…
edit: So where is the typical place to “draw the line” for serialization. I quickly started running into system classes I am using that aren’t able to be serialized, basically anything to do with the WPF or controls. So I can’t serialize my views, but probably just the viewmodels? I sort of expected that, but I just want to make sure I am not missing anything.
Ah yes,
I am following your example on github pretty closely. Its working pretty well so far, just have to write some code to regenerate all of the views from the de-serialized viewmodels.
Ok,
Still going pretty well, just brute force serializing almost everything and rebuilding what I can’t. It seems that Rhino.Collections.ArchivableDictionary is not marked as Serializable?
Do I need to mark this as non-serializable and then recreate the dictionary from the geometry? Basically I copy the UserDictionary from the geometry into my class at some point. Or is there another way to tell it to store this info?
edit: I decided to just convert to system dicts anytime I read in archivable dicts, as at that point I don’t need the association to the geometry anymore.
The plugin is fully serializing now… that was surprisingly easy.
I cam across another hopefully easy question. I see there is an event RhinoDoc.EndOpenDocument…I thought this would be a good place to call my method which rebuilds my UI from the deserialized data. It works fine, but I noticed that the RhinoDoc at this point(note this was also an issue with args on PlugIn.ReadDocument) has its Name property set to “”. Its like it hasn’t quite opened the document enough to get the Name? When I reopen my file I need to do some things based on the name(say for example the file was renamed, or moved).
Is there a specific point in the loading process where I can safely get the RhinoDoc.Name? For example when I call my Command, it is there. But my plan was to call the command on startup, triggered by an event…but so far these events seem too early to get the Name.
When your EndOpenDocument handler is called, set a flag on your plug-in indicating such. Then in an RhinoApp.Idle event handler, check your plug-in’s flag and then do whatever is necessary. Don’t forget to clear the flag on the out…
its a plugin for simulating CNC/robotic tools in rhino. Its been through several major rewrites over the last 6 years, from rhinoscript, to python, to C#. Its now a full wpf gui and pretty full featured, but serialization is going to be a huge help(we actually had this under python, with pickle, but there was no serialization of rhinocommon through pickle…so C# wins there again). And so far seems to be the easiest feature ever added…honestly I am pretty impressed with how easy it is to do this in the framework.