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:
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:
... 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); } ...
... 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); } ...
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:
... private bool m_absolute = false; public bool Absolute { get { return m_absolute; } set { m_absolute = value; if ((m_absolute)) { Message = "Absolute"; } else { Message = "Standard"; } } } ...
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.
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.
... 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); } ...
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().
... 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:
... private void Menu_AbsoluteClicked(object sender, EventArgs e) { RecordUndoEvent("Absolute"); Absolute = !Absolute; ExpireSolution(true); } ...