Click or drag to resize

Custom Component Options (C#)

This article discusses how to add custom options to a component and have them included in *.gh/*.ghx (de)serialization. It skips over some portions of Component design which have already been handled in previous topics, so do not read this article before familiarizing yourself with the My First Component topic.

The component we'll create in this article will sort a list of numbers and have the custom option to convert those numbers to absolute values prior to sorting. However, rather than providing this option as a boolean input parameter, we'll allow people to set it via the Component context menu. We'll need to do four special things to achieve this, to wit:

  • Declare a class level variable/property.
  • Provide access to the variable from within the Component menu.
  • Include the variable in (de)serialization.
  • Record undo events when changing the value.

Before you start with this topic, create a new class that derives from GH_Component, as outlined in the My First Component topic.

This component will require one input parameter and one output parameter, both of type Number with list access:

C#
...
  protected override void RegisterInputParams(Kernel.GH_Component.GH_InputParamManager pManager)
  {
    pManager.AddNumberParameter("Values", "V", "Values to sort", GH_ParamAccess.list);
  }
  protected override void RegisterOutputParams(Kernel.GH_Component.GH_OutputParamManager pManager)
  {
    pManager.AddNumberParameter("Values", "V", "Sorted values", GH_ParamAccess.list);
  }
...
Assuming for now we'll have a local property called Absolute() which gets a single boolean, we can also already write the SolveInstance() method:
C#
...
  protected override void SolveInstance(Kernel.IGH_DataAccess DA)
  {
    List<double> values = new List<double>();
    if ((!DA.GetDataList(0, values)))
      return;
    if ((values.Count == 0))
      return;

    // Don't worry about where the Absolute property comes from, we'll get to it soon.
    if ((Absolute))
    {
      for (Int32 i = 0; i < values.Count; i++)
      {
        values(i) = Math.Abs(values(i));
      }
    }

    values.Sort();
    DA.SetDataList(0, values);
  }
...

Class Level variables

The 'Absolute' option for this component applies to the entire object, but not to other instances of this component. Since it needs to survive (i.e. retain its value) for as long as the component lives, it has to be declared as a class level variable:

C#
...
private bool m_absolute = false;
public bool Absolute
{
  get { return m_absolute; }
  set
  {
    m_absolute = value;
    if ((m_absolute))
    {
      Message = "Absolute";
    }
    else
    {
      Message = "Standard";
    }
  }
}
...
The m_absolute field is a private field (only accessible from within this component) and it is exposed publicly via the Absolute() property, which allows both getting and setting. Furthermore, whenever the m_absolute field is set, the Absolute() property ensures that the correct message is assigned. The Message field on GH_Component allows you to set a string which will be displayed underneath the component on the canvas. This is to signal to users that there's an option they can change which is not directly accessible via the input parameters. Note that the message is not set until the Absolute() property is accessed, so you should specifically place a call to Absolute = False (or True) in the constructor.

It is of course possible to add any number of custom fields to a component, but you can only attach a single message, if you have more than one field you want to make the user aware of, you'll need to get creative.

(De)serialization of custom data

When you add options or states to your component which need to be 'sticky', you'll also need to (de)serialize them correctly. (De)serialization is used when saving and opening files, when copying and pasting objects and during undo/redo actions. In this particular case, we only need to add a single boolean to the standard file archive. Serialization in Grasshopper happens using the GH_IO.dll methods and types, not via standard framework mechanisms such as the SerializableAttribute.

Override the Write and Read methods on GH_Component and be sure to always call the base implementation.

C#
...
public override bool Write(GH_IO.Serialization.GH_IWriter writer)
{
  // First add our own field.
  writer.SetBoolean("Absolute", Absolute);
  // Then call the base class implementation.
  return base.Write(writer);
}
public override bool Read(GH_IO.Serialization.GH_IReader reader)
{
  // First read our own field.
  Absolute = reader.GetBoolean("Absolute");
  // Then call the base class implementation.
  return base.Read(reader);
}
...

Context menu changes

We'll also need to add an additional menu item to the component context menu, then handle the click event for that item. Adding items to a context menu is best done via the AppendAdditionalComponentMenuItems method. It allows you to insert anu number of item in between the Bake and the Help items. The easiest way to add menu items is to use the Shared methods on GH_DocumentObject such as Menu_AppendItem or one of the overloads. In this case we also want to assign a tooltip text to the item which cannot be done from inside Menu_AppendItem().

C#
...
protected override void AppendAdditionalComponentMenuItems(System.Windows.Forms.ToolStripDropDown menu)
{
  // Append the item to the menu, making sure it's always enabled and checked if Absolute is True.
  ToolStripMenuItem item = Menu_AppendItem(menu, "Absolute", Menu_AbsoluteClicked, true, Absolute);
  // Specifically assign a tooltip text to the menu item.
  item.ToolTipText = "When checked, values are made absolute prior to sorting.";
}
...

When this menu item is clicked, the delegate assigned inside the Menu_AppendItem() method will be invoked. It is here that we must handle a click event. There are usually three steps involved in handling clicks; Record the current state as an undo event, change the state, trigger a new solution:

C#
...
private void Menu_AbsoluteClicked(object sender, EventArgs e)
{
  RecordUndoEvent("Absolute");
  Absolute = !Absolute;
  ExpireSolution(true);
}
...
Since our Write() and Read() methods handle the (de)serialization of the Absolute field, we can use the default RecordUndoEvent method. It is possible to define your own undo records, but that is a topic for another day.