Serializing Data in Custom Attribute Class

Good morning. I am hoping someone can humour me by helping out with a problem I am having.

I have made a slider component with custom attributes in visual studio:
Screenshot 2021-12-21 093037
The reason is because its behavior differs slightly from the default slider and knob components.

The output value in class inheriting GH_Param serializes with no problem. However, the slider value in class inheriting GH_Attributes always resets upon file reopen or pasting. I will continue to try to untangle this myself but I would be so grateful if anybody decided to give input on this problem.

Thank you.

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;

public class Looping_SliderComponent : GH_Param<GH_Number>
{
    public Looping_SliderComponent() :
      base(new GH_InstanceDescription("Looping Slider", "Nickname", "Description", "Params", "Input"))
    { }

    public override void CreateAttributes()
    {
        m_attributes = new Looping_SliderComponentAttributes(this);
    }

    protected override Bitmap Icon
    {
        get
        {
            //TODO: return a proper icon here.
            return null;
        }
    }

    public override GH_Exposure Exposure
    {
        get
        {
            return GH_Exposure.primary | GH_Exposure.obscure;
        }
    }

    public override System.Guid ComponentGuid
    {
        get { return new Guid("15904C26-A453-4331-AE26-E543CCF3968E"); }
    }

    private double m_value = 6;
    public double Value
    {
        get { return m_value; }
        set { m_value = value; }
    }

    protected override void CollectVolatileData_Custom()
    {
        VolatileData.Clear();
        AddVolatileData(new Grasshopper.Kernel.Data.GH_Path(0), 0, new GH_Number(Value));
    }

    public override bool Write(GH_IO.Serialization.GH_IWriter writer)
    {
        writer.SetDouble("Value", m_value);
        return base.Write(writer);
    }

    public override bool Read(GH_IO.Serialization.GH_IReader reader)
    {
        m_value = 0;
        reader.TryGetDouble("Value", ref m_value);
        return base.Read(reader);
    }
}

public class Looping_SliderComponentAttributes : GH_Attributes<Looping_SliderComponent>
{
    public Looping_SliderComponentAttributes(Looping_SliderComponent owner)
      : base(owner)
    {
    }

    public override bool HasInputGrip { get { return false; } }
    public override bool HasOutputGrip { get { return true; } }

    private double mouseX;
    private bool clickingX;
    private bool mouseOverX;
    private double changeX;
    private double startX;
    private Region xRegion;

    //Our object is always the same size, but it needs to be anchored to the pivot.
    protected override void Layout()
    {
        //Lock this object to the pixel grid. 
        //I.e., do not allow it to be position in between pixels.
        Pivot = GH_Convert.ToPoint(Pivot);
        Bounds = new RectangleF(Pivot, new SizeF(200, 30));
    }

    public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (xRegion.IsVisible(e.CanvasLocation))
            {
                mouseX = e.CanvasX;
                clickingX = true;
                Owner.ExpireSolution(true);
                return GH_ObjectResponse.Capture;
            }
        }
        return base.RespondToMouseDown(sender, e);
    }

    public override GH_ObjectResponse RespondToMouseMove(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (clickingX)
        {
            changeX = e.CanvasX - mouseX;
            Owner.ExpireSolution(true);
            return GH_ObjectResponse.Ignore;
        }
        if (xRegion.IsVisible(e.CanvasLocation))
        {
            mouseOverX = true;
            return GH_ObjectResponse.Capture;
        }
        if (mouseOverX)
        {
            mouseOverX = false;
            return GH_ObjectResponse.Release;
        }
        return base.RespondToMouseMove(sender, e);
    }

    public override GH_ObjectResponse RespondToMouseUp(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (clickingX)
            {
                startX += changeX;
                changeX = 0;
                clickingX = false;
                Owner.ExpireSolution(true);
                return GH_ObjectResponse.Release;
            }
        }
        return base.RespondToMouseUp(sender, e);
    }

    public override void SetupTooltip(PointF point, GH_TooltipDisplayEventArgs e)
    {
        base.SetupTooltip(point, e);
        e.Description = "Double click to set a new integer";
    }
    
    protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
    {
        if (channel == GH_CanvasChannel.Objects)
        {
            double posX = startX + changeX;
            double myValue = Math.Round(posX / 50, 2);
            Owner.Value = myValue;
            
            //Drawing tools.
            Brush defaultGray = new SolidBrush(Color.FromArgb(255, 50, 50, 50));

            //Render output grip.
            GH_CapsuleRenderEngine.RenderOutputGrip(graphics, canvas.Viewport.Zoom, OutputGrip, true);

            //Render capsules.
            RectangleF scrollCanvas = new RectangleF(Pivot.X, Pivot.Y, 200, 30);
            GH_Capsule scrollCanvasCapsule = GH_Capsule.CreateCapsule(scrollCanvas, GH_Palette.White, 30, 0);
            scrollCanvasCapsule.Render(graphics, Selected, Owner.Locked, false);
            scrollCanvasCapsule.Dispose();

            RectangleF textCanvas = new RectangleF(Pivot.X + 20, Pivot.Y + 33, 60, 20);
            GH_Capsule textCanvasCapsule = GH_Capsule.CreateCapsule(textCanvas, GH_Palette.White, 10, 0);
            textCanvasCapsule.Render(graphics, Selected, Owner.Locked, false);
            textCanvasCapsule.Dispose();

            //Render graphics.
            RectangleF bounds = new RectangleF(Pivot.X + 20, Pivot.Y + 5, 160, 20);
            GraphicsPath boundsPath = new GraphicsPath();
            boundsPath.AddRectangle(bounds);
            xRegion = new Region(boundsPath);

            for (int xLoop = 0; xLoop < 6; xLoop++)
            {
                double count = 1 + Math.Floor(Math.Abs(posX) / 100);
                double pos = (Pivot.X) + (((count * 100) + posX + xLoop * 30) % 180);
                RectangleF balls = new RectangleF((float)pos, Pivot.Y + 5, 20, 20);
                graphics.FillEllipse(Brushes.Azure, balls);
            }
            
            graphics.DrawString(Math.Round(myValue, 2).ToString(), GH_FontServer.Standard, defaultGray, new PointF(Pivot.X + 28, Pivot.Y + 34));

        }
    }
}

here is an example

    public sealed class DM_Person
    {
        //field
        private string name;
        private int age;
        private int year;
        private double[] money;

        //default
        public DM_Person()
        { }

        //construction
        public DM_Person(string Name, int Age, int Year, double[] Money)
        {
            name = Name;
            age = Age;
            year = Year;
            money = Money;
        }

        //propority
        public string Name
        {
            get => name;
            set => name = value;
        }

        public int Age
        {
            get => age;
            set => age = value;
        }

        public int Year
        {
            get => year;
            set => year = value;
        }

        public double[] Money
        {
            get => money;
            set => money = value;
        }
        //function
        public override string ToString()
        {
            //return string.Format("\"{0}\" {1} {2}", Name, Age, Year);
            return string.Format("DM_Person");
        }

    }

    /// <summary>
    /// An IGH_Goo wrapper around Foo.
    /// </summary>
    public sealed class DM_PersonGoo : GH_Goo<DM_Person>
    {
        #region constructors
        public DM_PersonGoo()
        : this(null)
        { }
        public DM_PersonGoo(DM_Person person)
        {
            Value = person;
        }

        public override IGH_Goo Duplicate()
        {
            // It's okay to share the same Foo instance since Foo is immutable.
            return new DM_PersonGoo(Value);
        }
        #endregion

        #region properties
        public override string ToString()
        {
            if (Value == null) return "No person";
            return Value.ToString();
        }

        public override string TypeName => "DM_Person";
        public override string TypeDescription => "DM_Person's data";
        public override bool IsValid
        {
            get
            {
                if (Value == null) return false;
                if (Value.Name == null) return false;
                if (Value.Age < 0) return false;
                if (Value.Year < 0) return false;
                if (Value.Money == null) return false;
                return true;
            }
        }
        public override string IsValidWhyNot
        {
            get
            {
                if (Value == null) return "No data";
                if (Value.Name == null) return "No text data";
                if (Value.Age < 0) return "Negative integer data";
                if (Value.Year < 0) return "Negative integer data";
                if (Value.Money == null) return "No data";
                return string.Empty;
            }
        }

        

        public override bool CastFrom(object source)
        {
            if (source == null) return false;

            if (source is string name)
            {
                Value = new DM_Person(name, 0, 0, null);
                return true;
            }
            if (source is GH_String ghName)
            {
                Value = new DM_Person(ghName.Value, 0, 0, null);
                return true;
            }
            if (source is int age)
            {
                Value = new DM_Person(string.Empty, age, 0, null);
                return true;
            }
            if (source is GH_Integer ghAge)
            {
                Value = new DM_Person(string.Empty, ghAge.Value, 0, null);
                return true;
            }

            if (source is int year)
            {
                Value = new DM_Person(string.Empty, 0, year, null);
                return true;
            }
            if (source is GH_Integer ghYear)
            {
                Value = new DM_Person(string.Empty, 0, ghYear.Value, null);
                return true;
            }

            if (source is double[] money)
            {
                Value = new DM_Person(string.Empty, 0, 0, money);
                return true;
            }

            if (source is GH_Number[] ghMoney)
            {
                double[] moneyArray = null;
                for (int i = 0; i < ghMoney.Length; i++) moneyArray[i] = ghMoney[i].Value;
                Value = new DM_Person(string.Empty, 0, 0, moneyArray);
                return true;
            }

            return false;
        }
        public override bool CastTo<TQ>(ref TQ target)
        {
            if (Value == null)
                return false;
            if (typeof(TQ) == typeof(string))
            {
                target = (TQ)(object)Value.Name;
                return true;
            }
            if (typeof(TQ) == typeof(GH_String))
            {
                target = (TQ)(object)new GH_String(Value.Name);
                return true;
            }

            if (typeof(TQ) == typeof(int))
            {
                target = (TQ)(object)Value.Age;
                target = (TQ)(object)Value.Year;

                return true;
            }
            if (typeof(TQ) == typeof(GH_Integer))
            {
                target = (TQ)(object)new GH_Integer(Value.Age);
                target = (TQ)(object)new GH_Integer(Value.Year);

                return true;
            }
            if (typeof(TQ) == typeof(double))
            {
                target = (TQ)(object)Value.Age;
                target = (TQ)(object)Value.Year;

                return true;
            }
            if (typeof(TQ) == typeof(GH_Number))
            {
                target = (TQ)(object)new GH_Number(Value.Age);
                target = (TQ)(object)new GH_Number(Value.Year);

                return true;
            }

            if (typeof(TQ) == typeof(double[]))
            {
                target = (TQ)(object)Value.Money;
                return true;
            }

            if (typeof(TQ) == typeof(GH_Number[]))
            {
                target = (TQ)(object)Value.Money;
                return true;
            }

            return false;
        }
        #endregion

        #region (de)serialisation
        private const string IoTextKey = "Name";
        private const string IoAgeKey = "Age";
        private const string IoYearKey = "Year";
        private const string IoMoneyKey = "Money";

        public override bool Write(GH_IWriter writer)
        {
            if (Value != null)
            {
                if (Value.Name != null) writer.SetString(IoTextKey, Value.Name);
                if (Value.Age != 0) writer.SetInt32(IoAgeKey, Value.Age);
                if (Value.Year != 0) writer.SetInt32(IoYearKey, Value.Year);
                if (Value.Money != null) writer.SetDoubleArray(IoMoneyKey, Value.Money);
            }
            return true;
        }
        public override bool Read(GH_IReader reader)
        {

            string name = null;
            if (reader.ItemExists(IoTextKey)) name = reader.GetString(IoTextKey);
            int age = 0;
            if (reader.ItemExists(IoAgeKey)) age = reader.GetInt32(IoAgeKey);
            int year = 0;
            if (reader.ItemExists(IoYearKey)) year = reader.GetInt32(IoYearKey);
            double[] money= null;
            if (reader.ItemExists(IoMoneyKey)) money = reader.GetDoubleArray(IoMoneyKey);

            Value = new DM_Person(name, age, year, money);

            return true;
        }
        #endregion
    }

Thank you very much for the code that you supplied. I ended up looking elsewhere for a solution, though.

I have solved my problem, and the solution is that I now know more about the way that data moves through c# classes, and how the GH_IO read and write things work. Here is the finished plugin in case anybody is curious about it:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;

public class Looping_SliderComponent : GH_Param<GH_Number>
{
    public Looping_SliderComponent() :
      base(new GH_InstanceDescription("Looping Slider", "Nickname",
                                      "Description",
                                      "Params", "Input")) { }

    public override void CreateAttributes()
    {
        m_attributes = new Looping_SliderComponentAttributes(this);
    }

    protected override Bitmap Icon
    {
        get
        {
            //TODO: return a proper icon here.
            return null;
        }
    }

    public override GH_Exposure Exposure
    {
        get
        {
            return GH_Exposure.primary | GH_Exposure.obscure;
        }
    }

    public override System.Guid ComponentGuid
    {
        get { return new Guid("15904C26-A453-4331-AE26-E543CCF3968E"); }
    }

    private double m_value = 6;
    public double Value
    {
        get { return m_value; }
        set { m_value = value; }
    }

    protected override void CollectVolatileData_Custom()
    {
        VolatileData.Clear();
        AddVolatileData(new Grasshopper.Kernel.Data.GH_Path(0), 0, new GH_Number(Value));
    }

    public override bool Write(GH_IO.Serialization.GH_IWriter writer)
    {
        writer.SetDouble("Value", m_value);
        return base.Write(writer);
    }

    public override bool Read(GH_IO.Serialization.GH_IReader reader)
    {
        m_value = 0;
        reader.TryGetDouble("Value", ref m_value);
        return base.Read(reader);
    }
}

public class Looping_SliderComponentAttributes : GH_Attributes<Looping_SliderComponent>
{
    public Looping_SliderComponentAttributes(Looping_SliderComponent owner)
      : base(owner)
    {
    }

    public override bool HasInputGrip { get { return false; } }
    public override bool HasOutputGrip { get { return true; } }

    private double direction;
    private double mouseX;
    private bool clickingX;
    private bool mouseOverX;
    private double changeX;
    private double startX;
    private Region xRegion;

    //Our object is always the same size, but it needs to be anchored to the pivot.
    protected override void Layout()
    {
        //Lock this object to the pixel grid. 
        //I.e., do not allow it to be position in between pixels.
        Pivot = GH_Convert.ToPoint(Pivot);
        Bounds = new RectangleF(Pivot, new SizeF(200, 30));
    }

    public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (xRegion.IsVisible(e.CanvasLocation))
            {
                //mouseX = e.CanvasX;
                clickingX = true;
                Owner.ExpireSolution(true);
                return GH_ObjectResponse.Capture;
            }
        }
        return base.RespondToMouseDown(sender, e);
    }

    public override GH_ObjectResponse RespondToMouseMove(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (clickingX)
        {
            direction = mouseX - e.CanvasX;
            if (direction > 0)
            {
                Owner.Value--;
            }
            if (direction < 0)
            {
                Owner.Value++;
            }
            mouseX = e.CanvasX;
            Owner.ExpireSolution(true);
            return GH_ObjectResponse.Ignore;
        }
        if (xRegion.IsVisible(e.CanvasLocation))
        {
            mouseOverX = true;
            return GH_ObjectResponse.Capture;
        }
        if (mouseOverX)
        {
            mouseOverX = false;
            return GH_ObjectResponse.Release;
        }
        return base.RespondToMouseMove(sender, e);
    }

    public override GH_ObjectResponse RespondToMouseUp(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (clickingX)
            {
                changeX = 0;
                clickingX = false;
                Owner.ExpireSolution(true);
                return GH_ObjectResponse.Release;
            }
        }
        return base.RespondToMouseUp(sender, e);
    }

    public override void SetupTooltip(PointF point, GH_TooltipDisplayEventArgs e)
    {
        base.SetupTooltip(point, e);
        e.Description = "Double click to set a new integer";
    }
    
    protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
    {
        if (channel == GH_CanvasChannel.Objects)
        {
            double posX = startX + changeX;

            //Drawing tools.
            Brush defaultGray = new SolidBrush(Color.FromArgb(255, 50, 50, 50));

            //Render output grip.
            GH_CapsuleRenderEngine.RenderOutputGrip(graphics, canvas.Viewport.Zoom, OutputGrip, true);

            //Render capsules.
            RectangleF scrollCanvas = new RectangleF(Pivot.X, Pivot.Y, 200, 30);
            GH_Capsule scrollCanvasCapsule = GH_Capsule.CreateCapsule(scrollCanvas, GH_Palette.White, 30, 0);
            scrollCanvasCapsule.Render(graphics, Selected, Owner.Locked, false);
            scrollCanvasCapsule.Dispose();

            RectangleF textCanvas = new RectangleF(Pivot.X + 20, Pivot.Y + 33, 60, 20);
            GH_Capsule textCanvasCapsule = GH_Capsule.CreateCapsule(textCanvas, GH_Palette.White, 10, 0);
            textCanvasCapsule.Render(graphics, Selected, Owner.Locked, false);
            textCanvasCapsule.Dispose();

            //Render graphics.
            RectangleF bounds = new RectangleF(Pivot.X + 20, Pivot.Y + 5, 160, 20);
            GraphicsPath boundsPath = new GraphicsPath();
            boundsPath.AddRectangle(bounds);
            xRegion = new Region(boundsPath);

            for (int xLoop = 0; xLoop < 6; xLoop++)
            {
                double count = 1 + Math.Floor(Math.Abs(posX) / 100);
                double pos = (Pivot.X) + (((count * 100) + posX + xLoop * 30) % 180);
                RectangleF balls = new RectangleF((float)pos, Pivot.Y + 5, 20, 20);
                graphics.FillEllipse(Brushes.Azure, balls);
            }
            
            graphics.DrawString(Math.Round(Owner.Value, 2).ToString(), GH_FontServer.Standard, defaultGray, new PointF(Pivot.X + 28, Pivot.Y + 34));
        }
    }
}
1 Like