C# - Wrapping Rh geometry makes class member fields "stale" - Why?

So I’m making a little Framework for my Rhino/Grasshopper development, and for this I’m wrapping some simple types (double, int, bool etc) into my own “CoAttributeMember” classes, with names like Co_double, Co_int, Co_bool etc.

These classes have some fancy features like Observer pattern, Derived values, Lazy evaluation, etc. So far so good.

Then I also wrapped some Rhino geometry, like Point3d, Vector3d, Brep, Curve and Line (into Co_point3d, Co_Brep, Co_Curve, Co_Line etc), and then suddenly any class field values or property values in those wrapper classes can’t be modified (I can assign a value, but the Filed value doesn’t actually change(!)).

Say I have these two classes, a Co_int and a Co_Line class. If I try to modify the state property IsValueChanged in these (like, from false to true), then the value chanages (as expected) in the Co_int class, but not in the Co_Point3d class. < scratching head >

Thus it seems this is a consistent problem with any Co-class which wraps any Rhino Geometry, but not the C# simple types! Why would that be the case?

Using VS2022, RhinoCommon R7 and .NET Framework 4.8.

Any ideas about why this is happening?

//Rolf

Code examples:
So these two CoAttributeMember classes are “identical” except for the wrapped types (Point3d & int), but trying to modify the property IsValueChanged fails on the Co_Point3d (and on all other classes wrapping a Rhino geometry type) but not on the Co_int class!.

    public class Co_Point3d : CoAttributeMember
    {
        public Co_Point3d()
        {
            this.SetObject(Point3d.Unset);
        }
        public bool IsValueChanged;
        private Point3d _Point; //cache
        public Point3d Value
        {
            get {
                if (!this.IsValueChanged) return this._Point;

                this._Point = (Point3d)GetObject();
                this.UnsetValueChanged();
                return this._Point; // <-- cached
            }
            set => SetObject(value);
        }
    }
    public class Co_int : CoAttributeMember
    {
        public Co_int()
        {
            this.SetObject(int.MinValue);
        }
        public bool IsValueChanged;
        private int _Value; //cache
        public int Value
        {
            get {
                if (!this.IsValueChanged) return this._Value;

                this._Value = (int)GetValue();
                this.UnsetValueChanged();
                return this._Value; // <-- cached
            }
            set => SetValue(value);
        }
    }

I don’t know how you determine if a value has changed. This part is missing… Could you post the base implementation? The observer pattern is usually implemented differently, by using events. However I suspect that SetObject contains the problem and that the way you check equality is the problem. Other than that, why don’t you write a generic class if only the wrapped type changes?I would also suspect the “IsValueChanged” field inside the base class. I would also be careful, and prevent writing public fields. The point of the event system is that you inform all subscriber to pick up the new value . This way it’s also thread-safe and efficient!

1 Like

without knowing more or really understanding all you may be doing there, I would look at the possibility that a) copies of structs are being modified, leaving originals unchanged, and b) rhino objects will generally need CommitChanges() called

IsValueChanged = true;
IsValueChanged = false;

The value doesn’t change although the code runs.

The classes code I posted above is just to illustrate the two kinds “simple” types and the RH types, but the part that doesn’t work is the class field (IsValueChanged). It’s not in the base class.

Notice that I’ve tried direct fields (as in the code example) and with properties, and more, but it gives the same problem; The value doesn’t change, even if the code assigning the value runs fine. The change just doesn’t stick. On the ones that wraps Rhino types.

Other than that, I implement my own observer pattern, well tested for decades. No problem with that, only the problem described. Could the problem be related to the fact that the super class CoAttributeMember is in the Co_Framework.dll which I’m inheriting from?

// Rolf

Again we don’t see the full picture. I think if you put this flag into the derived class, but you set it in the base, then this is odd. If you have the same field inside the base, than the derived one shadows the base one, but it will not be the same flag… I doubt that it has anything todo with Rhinocommon or being inside a dll.

Except that there is duplicate of your base class which you accidentally using instead. E.g an older version. The best way to debug this is setting a breakpoint including jumping into decompiled sources . DNSpy is quite capable for this. But its hard to guess without getting a full example

See:

observer.gh (9.7 KB)

private void RunScript(object x, object y, ref object A)
{
    var point = new Point3d(3, 3, 3);
    var obsPt = new ObservablePoint(point);

    obsPt.Changed += (s,e) => Print("-> Point changed!");
    obsPt.NotChanged += (s,e) => Print("-> Point did not change!");

    Print("No change expected, initialized with same instance");
    obsPt.Object = point;
    Print("No change expected, because we copy a value type (struct)...");
    obsPt.Object = new Point3d(3, 3, 3);
    Print("Change expected, because we create a new point...");
    obsPt.Object = new Point3d(6, 5, 4);

    Print("--------------");

    var srf = NurbsSurface.CreateFromCorners(
      new Point3d(0, 0, 0),
      new Point3d(1, 0, 0),
      new Point3d(0, 1, 0),
      new Point3d(1, 1, 0));
    var obsSrf = new ObservableNurbsSurface(srf);
    obsSrf.Changed += (s,e) => Print("-> NurbsSurface changed!");
    obsSrf.NotChanged += (s,e) => Print("-> NurbsSurface did not change!");

    Print("No change expected, initialized with same instance");
    obsSrf.Object = srf;
    Print("Change expected, because we copy a reference type (class)...");
    obsSrf.Object = (NurbsSurface) srf.Duplicate();
    Print("Change expected, because we create a new surface...");
    obsSrf.Object = NurbsSurface.CreateFromCorners(
      new Point3d(0, 0, 1),
      new Point3d(1, 0, 1),
      new Point3d(0, 1, 1),
      new Point3d(1, 1, 1));
}

 public class ObservableNurbsSurface : Observable<NurbsSurface>
  {
    public ObservableNurbsSurface(NurbsSurface srf) : base(srf)
    {
    }

    public override bool ObjectEquals(NurbsSurface a, NurbsSurface b)
    {
      // or Surface.GeometryEquals
      return a.Equals(b);
    }
  }

  public class ObservablePoint : Observable<Point3d>
  {
    public ObservablePoint(Point3d point) : base(point)
    {
    }

    public override bool ObjectEquals(Point3d a, Point3d b)
    {
      return a.Equals(b);
    }
  }

  public abstract class Observable<T>
  {
    private T _object;

    public T Object
    {
      get {return _object;}
      set
      {
        if (!ObjectEquals(_object, value))
        {
          _object = value;
          Notify();
        }
        else
        {
          NotifyNoChange();
        }
      }
    }

    public event EventHandler Changed;
    public event EventHandler NotChanged;

    public Observable(T obj)
    {
      _object = obj;
    }

    public void Notify()
    {
      Changed.Invoke(this, EventArgs.Empty);
    }

    public void NotifyNoChange()
    {
      NotChanged.Invoke(this, EventArgs.Empty);
    }

    public abstract bool ObjectEquals(T a, T b);

  }
  
  

yields:

No change expected, initialized with same instance
→ Point did not change!

No change expected, because we copy a value type (struct)…
→ Point did not change!

Change expected, because we create a new point…
→ Point changed!


No change expected, initialized with same instance
→ NurbsSurface did not change!

Change expected, because we copy a reference type (class)…

→ NurbsSurface changed!

Change expected, because we create a new surface…
→ NurbsSurface changed!

2 Likes

Thank you for your attention TomTom, but the problem isn’t the property implementation, the subscribers, or the events, the problem is a field OUTSIDE the property being “stale” (although being involved in the property as a state holder)

I need to repeat the fact that the super class for these properties (CoAttributeMember) wraps properties of different C# types, like int, double, etc. Which works fine. But it also implements Rhino types, like Line, Point3d, Plane etc. And only when the type of the wrapped property is from the Rhino API, only then the CoAttributeMember.IsValueChanged field fails.

The property wrappers and the base class for these property wrappers (CoAttributeMember) is imported from my Co_Framework.dll.

To illustrate the difference I’ll try again to show two more elaborate pseudo cases:

A C# class implementing two CoAttributes (Co_double and Co_Plane), thus implementing two different property types; One property implementing a simple C# type (a double) and the other property implementing a Rhino type (a Plane). But in the case of the Rhino type the Rhino property type fails to modify the field IsValueChanged, which isn’t involved in the property itself (although both Co attributes are based on the same CoAttributeMember class)

So in my example codes in this thread, the field “IsValueChanged” field is there only to spare you from showing the actual state (storing the state in a “CoAttributeMember.StateFlag” (int) property by bit-masking, an int in which I mask-in the bit representing the “dirty state”). I’m only simplifying that state by using a bool flag (IsValueChanged = true/false;). But both the bit-flag and the bool field fails in a similar manner; The value doesn’t stick.

Again, the problem is the same whether I write the state to the Bitflag or to the temp IsValueChanged field. What is relevant though is that in both cases they both belong to the CoAttribute member, so the only difference wbteen a working property and a malfunctioning property is that the malfunctioning property is a Rhino type (I can’t make this fact any clearer).

So if the CoAttributeMember based property wrapper wraps a regular double, int or bool, then there is no problem. Everything works normally, even writing to other fields in that class.

Following below I’m - in a symbolic and simplified manner - trying to express the same thing as above:

public class Co_double: CoAttributeMember
{
    public bool IsValueChanged;
    public double _Value
    public double Value
    {
        get => _Width;
        set => _Width = value;
    }
}

Now, using the above property class (Co_double) in some other C# class works fine:

public Co_double m_Width;
public double Width
{
     get  => m_Width.Value;            // works fine
     set  => m_Width.Value = value;    // works fine
}

m_Width.IsValueChanged = true or false; // Works fine.

Changing the value of IsValueChanged works (changes sticks), but if the property type is from the Rhino API, say a Plane, the exact same code does NOT work (the value change doesn’t stick, although no errors are reported.

Now look at the case that does not work: The following is EXACTLY the same code as the above, except for now dealing with a Rhino type (Plane, or whatever from the RhinoCommon):

public class Co_Plane: CoAttributeMember
{
    public bool IsValueChanged;
    public Plane _Value
    public Plane Value
    {
        get => _Value;
        set => _Value = value;
    }
}

Again, using the above class to implement a property in some other C# class:

public Co_Plane m_StartPlane;
public Plane StartPlane
{
     get  => m_StartPlane.Value;            // works fine
     set  => m_StartPlane.Value = value;    // works fine
}

// Modifying the flag below does NOT work! (new value doesn’t stick):
m_StartPlane.IsValueChanged = true; 

Si? Same kind of property (based on the same super class) but holding different types. And if the type being involved is a Rhino type, then the class (Co_Plane, or Co_Line, or Co_Curve, or Co_Brep) starts behave strangely (fields gets stale).

Notice that the failing members are not the in property implementation itself. They are “outside” of the property implementation (the “stale” flags only makes the property fail to update properly, but the reason for that is the failing flag).

// Rolf

Rolf, since the code is incomplete, I can only guess, but here are some more ideas:

A.) The code never reaches the line where you set the flag. Can you set the breakpoint and verify that it actually performs it? Maybe there is a silent error.
B.) Something else is setting it back. You might trigger something more than once. Usually when you do something from a GUI, you might not be aware of the fact that when something is refreshing it will cause more than one execution. For the first time it does it correctly, in a second pass it defaults it to false.
C.) You reference a wrong, outdated dll. Decompile your own dll to see if its really the code you are developing.
D.) You encounter a caching/event-related bug. Are you cleaning up/unsubscribing from events properly?
E.) Mixing reference with value type, although a Point3d is also a value type.
F.) A Heisenbug, you only see it while observing. Indicating that your debugging technique is faulty
G.) You have a IsValueChanged field also in your base. You set it indirectly in the base, and now you mix both properties. Although then it should also fail for base types…

You might solve it by creating a fully working minimal example for us… :slight_smile:

1 Like

Hi again @TomTom, sorry for responding so slowly, but I had a code review with the domain expert for two days.

By isolating code I have excluded the subscription & notification methods [edit: as being involved in the cause of the problem] (by simply commenting out all other code), and then it works as it should.

Well, I already indicated that it would do so by the simple fact that the same (property) wrapper worked flawlessly for any other types than types from RhinoCommon.

I’m now reintroducing disabled code by the bisection-method and should have isolated the cause within hours.

I really appreciate the work you’ve done suggesting possible causes, and I apologize for not being clear enough in my examples about the fact that the subscription/notifications were not the main suspects.

Back to the man-cave with a big cup of coffee. :wink:

//Rolf