Failing to Read UserData

Hey @dale
< FYI - My code is F#, but I’m using dotPeek on the .rhp file to get the equivalent C#. If a variable name is generated, I do a quick notepad find and replace with something friendlier.>

I’m adding a HistoryIndex to UserData, but Find() always returns null (Unit) when I try to read it during the replay:

public override Unit replayChildMissing(ReplayHistoryResult r)
{
	if (!(r.ExistingObject.UserData.Find(typeof (HistoryIndex)) is HistoryIndex historyIndex))
	  return (Unit) null;
	if (historyIndex.Index >= this.updatedObjs.Length)
	  return (Unit) null;
	ReplayHistoryResult replayHistoryResult = r;
	GeometryBase updatedObj = this.updatedObjs[historyIndex.Index];
	ObjectAttributes attributes = replayHistoryResult.ExistingObject.Attributes;
	...
}

This UserData is to support when a users deletes one of the children, I can update the remaining ones. The standard ReplayHistory() sample code shows something like this, where if the lengths don’t match you fail without updating history:


  if (updatedObjs != replay.Results.Length)
        return false;

If my lengths don’t match, I switch to that replayChildMissing() version where I have the original length and index for that child:


  public class HistoryIndex : Rhino.DocObjects.Custom.UserData
  {
    internal int Index;
    internal int Length;

I set those values when the children are added to the doc:

public override Unit addChildren(HistoryRecord hRecord, ObjectAttributes parentAttrib, int len, int idx, GeometryBase g)
  {
	HistoryIndex historyIndex = new HistoryIndex();
	historyIndex.SetValues(idx, len);
	parentAttrib.UserData.Add((Rhino.DocObjects.Custom.UserData) historyIndex);
	if (g is InstanceReferenceGeometry referenceGeometry)
	{
	  InstanceDefinition id = this.doc.InstanceDefinitions.FindId(referenceGeometry.ParentIdefId);
	  this.doc.Objects.AddInstanceObject(id.Index, referenceGeometry.Xform, parentAttrib, hRecord, id.IsReference);
	  return (Unit) null;
	}
	this.doc.Objects.Add(g, parentAttrib, hRecord, false);
	return (Unit) null;
  }

I can see the data in the debugger. Any ideas what’s going on?

Geez, I found it.

match r.ExistingObject.UserData.Find(typeof<HistoryIndex>) with

// should have been 

match r.ExistingObject.Attributes.UserData.Find(typeof<HistoryIndex>) with
5 Likes

You’re doing great Eric. Thanks for posting your solutions for others in the future :slight_smile:

It was only a few hours of hair pulling.

There is a bug in the above code sample I found after reading the HistoryIndex. I was taking the parent attribute, adding the replay index, then adding it to the child. I should have been doing this:

let attrib = attribParent.Duplicate()

The original sample set it on the parent, and UserData.Add() does not overwrite. You need to use an AddOrUpdate pattern (also ensuring I pass ObjectAttributes and not RhinoObject in the future):

let setData (attrib: ObjectAttributes) len idx =
	let setter (hi: HistoryIndex): HistoryIndex =
		hi.Index <- idx
		hi.Length <- len
		hi
	
	match attrib.UserData.Find(typeof<HistoryIndex>) with
	| :? HistoryIndex as hi ->
		setter hi |> ignore
	| _ ->
		new HistoryIndex() |> setter |> attrib.UserData.Add |> ignore

I do have a couple of follow on questions.

  1. Is this the proper way to handle commands where 1 parent → N children? All the rhino-developer-samples show to fail if the children length is off, but native Rhino command don’t. They add ghosts back to the file (Project and TweenCurves):

2024.06.08.Rhino_CS1sxADaIo

  1. I understand Rhino being liberal with passing UserData (Split copies data to both halves), but this is a situation where I do not want it passed on. Here I should remove the value on history break and tell UserData.OnDuplicate() not to copy the field, but I don’t see a way to do that.

Hi @EricM,

Is the question how to grow or shrink the number of children in ReplayHistory? Imagine dividing a curve by lenght and then later changing the curve’s length.

.NET UserData is always copied when the owning object is copied. If you don’t want this behavior, then you’ll have to remove the user data after the copy.

– Dale

1 Like

It’s a 1 to fixed N, like the example I showed with _Project and _TweenCurve. If you partially delete the N children, those commands’ history replays bring the deleted portion back from the dead.

I’m trying to avoid that. If I see the replay.Results.Length is off, I’m using HistoryIndex to skip over the deleted children.

I think there are two situations when I should clean up the HistoryIndex: 1) when history is broken, and 2) when a copy is made without the parent. How can I hook into these events?

I know about On_Duplicate, but it doesn’t fire when history is broken, it doesn’t give me any context around the copy, and even if it did provide context, I can only change the values (you can’t remove it). Most duplications should not copy the user data, except for imports/copy/paste with the parent.

@EricM - I think I have a better way of doing this. Give me some time to toss a sample together.

– Dale

1 Like

Hi @EricM,

See the following code sample. Note, you will need Rhino 8 SR9 to run.

TestHistoryDivide.cs (3.9 KB)

In this sample, the history record is set on objects after they are added to the document, not while. In delaying this, you have chance to add extra data to the record, such as the ids of the newly added (child) objects.

Durning history replay, you can compare the ids of those objects added with the command was run vs what is passed in the ReplayHistoryData object.

The sample is simple, but you should get the idea.

– Dale

I need to support v7 till the UI rework gets a bit more stable. What method call needs SR9?

Also, for copy/paste history, will the SetGUID values update to the new child GUIDs?