-
Notifications
You must be signed in to change notification settings - Fork 6
Coding details: MethodCalllTemplate
This template allows you to easily create new MethodCall components. A MethodCall component is a generic component that can expose any method exposed in the list offered in its menu. here's an example of the CreateBHoM component that implement the MethodCall template:
As you can see, the component start in a generic form without input or output. The user then select the method he wants to use from the component's menu and the components that update itself to match that method. The template itself takes care of most of the complexity involved in that process. The only thing required to create a new MethodCall component is to provide a GetRelevantMethods() function to define the methods that will be exposed. Let's get into the details on how we get to that.
We'll discuss the four meaningful parts of MethodCalllTemplate:
- Creating the menu that lists all the methods available
- Transforming the component to represent the method selected by the user
- Saving the component so it is still corresponding to the selected method once the file is re-opened
- Using the template to create new MethodCall components
The full code is available here
In term of filling the menu itself, nothing very complicated here. We get the method tree from the GetRelevantMethods() provided by the component implementing the template. This is a tree, not a list so the final component has control over the way the methods are organised. This template provides helper to create that tree though. We'll talk about that in the next section.
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
base.AppendAdditionalComponentMenuItems(menu);
if (m_Method == null)
{
Tree<MethodBase> methods = GetRelevantMethods();
AppendMethodTreeToMenu(methods, menu);
AppendSearchMenu(methods, menu);
}
}
AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
is provided by Grasshopper so we just need to override it. As you can see once, we have the tree of methods, we use it to create both the drop-down menu and the search box.
The creation of the drop-down menu is straight-forward WindowForm menu creation from a tree structure so the code should be self-explanatory. One thing to note though is that we are storing the link between the menu items (ToolStripMenyItem) and the methods (MethodBase) in this dictionary:
protected Dictionary<ToolStripMenuItem, MethodBase> m_MethodLinks = new Dictionary<ToolStripMenuItem, MethodBase>();
For those unused to it, notice that MethodBase
is a class definition from System.Reflection
. If you don't know about reflection in C#, it's probably best you do a bit of reading on that first.
Same thing for the search section. We first filter the list of method using the content of the search input (note that spaces are used as separator for multi-words search). We then add the remaining method to the menu below the search. As for the drop-down menu, we store the link between the menu items and the methods in m_MethodLinks
.
Two methods are provided to help you add a method to a tree:
-
AddMethodToTree(Tree<MethodBase> tree, MethodBase method)
. This uses the namespace followed by the type of the first argument (we use injection method, remember?) to define the path in the tree. This can be summarised in the two lines below (where typeName is the type of the first argument). We skip the first two parts of the namespace because they are always the same (BH.Engine):IEnumerable<string> path = method.DeclaringType.Namespace.Split('.').Skip(2).Concat(new string[] { typeName });
AddMethodToTree(tree, path, method);
-
AddMethodToTree(Tree<MethodBase> tree, IEnumerable<string> path, MethodBase method)
. This goes down the tree following teh path and creating branches when they don't exist yet. The tree node for the method itself is created using the name of the method and its parameters. We callGetMethodString(methodName, method.GetParameters());
for that.
One more method is provided to build the string representation of your method: GetMethodString(string methodName, ParameterInfo[] parameters)
. You can either override it if you are still using AddMethodToTree or call it directly if you are writing your own version of AddMethodToTree. That method is pretty self explanatory:
protected virtual string GetMethodString(string methodName, ParameterInfo[] parameters)
{
string name = methodName + "(";
if (parameters.Length > 0)
name += parameters.Select(x => x.Name).Aggregate((x, y) => x + ", " + y);
name += ")";
return name;
}
The transformation appends when a menu item is clicked:
protected void Item_Click(object sender, EventArgs e)
{
ToolStripMenuItem item = (ToolStripMenuItem)sender;
if (!m_MethodLinks.ContainsKey(item))
return;
m_Method = m_MethodLinks[item];
if (m_Method == null)
return;
this.NickName = m_Method.IsConstructor ? m_Method.DeclaringType.Name : m_Method.Name;
List<ParameterInfo> inputs = m_Method.GetParameters().ToList();
Type output = m_Method.IsConstructor ? m_Method.DeclaringType : ((MethodInfo)m_Method).ReturnType;
ComputerDaGets(inputs);
UpdateInputs(inputs, output);
}
The code is pretty straightforward:
- Recover the method from the menu item and the dictionary
m_MethodLinks
- Define the nickname of the component: if the method is a constructor, use the returned type, otherwise use the method name.
- Compute the methods that will recover the inputs from Grasshopper (using DA.GetData or DA.getDataList from GH).
- Create the inputs themselves (and the single output obviously).
The last two points are worth exploring further.
In order to create those methods, we will use two template methods, one for the object data and one for the list data:
public static T GetData<T>(IGH_DataAccess DA, int index)
{
IGH_Goo goo = null;
DA.GetData(index, ref goo);
return ConvertGoo<T>(goo);
}
/*************************************/
public static List<T> GetDataList<T>(IGH_DataAccess DA, int index)
{
List<IGH_Goo> goo = new List<IGH_Goo>();
DA.GetDataList<IGH_Goo>(index, goo);
return goo.Select(x => ConvertGoo<T>(x)).ToList();
}
Those two are just boiler plate around the DA.GetData methods provided by Grasshopper. They take care of unwrapping the data from its GH container and convert it if necessary (i.e. Rhino.Geometry to BHoM.Geometry).
So for each input (i.e. parameter of the function selected by the user), we apply the same logic:
- Define if the object is a list or a single object so we know if we need to use GetData or GetDataList
- Create an instance of the generic GetData[List] by setting T t othe type of that input.
- Storing that instance into
protected List<MethodInfo> m_DaGets = new List<MethodInfo>()
This part is taken of by the method UpdateInputs(List<ParameterInfo> inputs, Type output)
. The code in that method is a bit lengthy so we'll just summarize it here:
- For each input
- Get the object type (if a list, get the type of object stored by the list)
- Register the input parameter with Grasshopper (we'll talk more about that in the next section)
- Define the access type (list or object)
- Update the description Grasshopper provides when the mouse is over the input.
- Register the output parameter with Grasshopper
- Ask Grasshopper to refresh the component
The methods for registering an input/output are not really interesting since they are just taking care of Grasshopper details but they are worth mentioning for completing. They also call the GetGH_Param(Type type, string name)
. This method is in charge of defining the type of object supported by that input/output.
protected void RegisterInputParameter(Type type, string name, object defaultVal = null)
{
dynamic p = GetGH_Param(type, name);
if (defaultVal != null)
p.SetPersistentData(defaultVal);
Params.RegisterInputParam(p);
}
/*************************************/
protected void RegisterOutputParameter(Type type)
{
if (typeof(IBHoMGeometry).IsAssignableFrom(type))
Params.RegisterOutputParam(new Param_Geometry { NickName = "" });
else
Params.RegisterOutputParam(GetGH_Param(type, ""));
}
That bit is super trivial, we simply override the write and Read methods from the GH_Component we inherit from. The writer just add this set of info the base writer:
- Assembly qualified name of the type containing the method
- Name of the method represented by that component
- Number of parameters
- Type of each parameter (stored using the assembly qualified name)
The Reader is no much more difficult. It reads everything out and uses the info listed above to restore the component. That can be summarized by those 3 lines of code:
RestoreMethod(Type.GetType(typeString), methodName, paramTypes);
if (m_Method != null)
ComputerDaGets(m_Method.GetParameters().ToList());
The RestoreMethod function is in charge of looking through all the public methods of that type and selecting teh one that corresponds to the method name and list of parameters.
To wrap things up, let's look at an example of a class using this template:
public class QueryBHoM : MethodCallTemplate
{
/*******************************************/
/**** Properties ****/
/*******************************************/
public override Guid ComponentGuid { get; } = new Guid("63DA0CAC-87BC-48AC-9C49-1D1B2F06BE83");
protected override System.Drawing.Bitmap Internal_Icon_24x24 { get; } = Properties.Resources.Query;
public override GH_Exposure Exposure { get; } = GH_Exposure.secondary;
/*******************************************/
/**** Constructors ****/
/*******************************************/
public QueryBHoM() : base("Query BHoM Object", "QueryBHoM", "Query information about a BHoMObject", "Alligator", " Engine") {}
/*******************************************/
/**** Override Methods ****/
/*******************************************/
protected override Tree<MethodBase> GetRelevantMethods()
{
Tree<MethodBase> root = new Tree<MethodBase> { Name = "Select query" };
foreach (MethodBase method in BH.Engine.Reflection.Query.BHoMMethodList().Where(x => x.DeclaringType.Name == "Query"))
{
AddMethodToTree(root, method);
}
return root;
}
/*******************************************/
}
So if we ignore the usual definition of properties and constructor required for any Grasshopper component, the only thing left to do is to override the GetRelevantMethods()
.
- The tree is given the name that corresponds to what is written on the top level of the drop-down menu
-
BH.Engine.Reflection.Query.BHoMMethodList().Where(x => x.DeclaringType.Name == "Query")
is collecting all the methods that are stored in the "Query" class. -
AddMethodToTree
is the helper method we discussed earlier.