Introduction
This document is the forth in a set of tutorials on writing calculations in Service Desk. Each topic can be read independently but for an introduction to calculations please see the following document: Calculation Writing Tutorial - 1. What is a calculation? What is Boo?.
Intended audience
Some experience in object and process design is recommended, as well as the basic concept of calculations as covered in the first tutorial (linked above).
Working with Collections
Collections are the relationships that are one-to-many, for example on an Incident you have Assignments, Notes, Closures, etc. as collections because there could be multiple items of each on a single Incident.
Referencing collections in calculations vs. runtime values and placeholders
There is a general rule when referencing a collection in a runtime value or placeholder that the latest item in the collection will be returned. For example you may use an automatic Add Reminder action in an Incident process after adding a note to send that note to the assignee. Here you could use the placeholder {Notes/Text} to return the latest Note's Text attribute.
There is a common mis-conception that you can do the same in calculations but this is not the case. If you try to reference a collection attribute directly this will cause the calculation to fail:
import System static def GetAttributeValue(Incident): Value = Incident.Notes.Text return Value
This will fail with error message "Field 'Touchpaper.Framework.Data.DataObjectListProxy.Text' not found.". The reason is because there could be 20 Notes on this Incident and the calculation will not just give you the latest one automatically. This may seem like a limitation however with the right code calculations can access all of the items in a collection so you can get more more data than a runtime value or placeholder ever can.
Collection Functions
To get a single item from a collection there are some functions built in to the calculations editor to allow this. These allow you to get the first or last item in the collection, the number of items in the collection, or the minimum or maximum value of a specific attribute in the collection items.
Getting the first or last item in a collection
To replicate the behaviour of referencing a collection in a placeholder there is a function to return the latest collection item called Latest().
import System static def GetAttributeValue(Incident): Value = Incident.Notes.Latest() return Value.Text
In this example the variable called Value gets the whole Note assigned to it so you can access any of the attributes below it using Value.Text, Value.CreationDate, etc. If you only need to access one attribute on the collection object you can reference it in the same line as the Latest() function:
import System static def GetAttributeValue(Incident): Value = Incident.Notes.Latest().Text return Value
Note that the .Latest() is before the .Text and not after it. The exact same techniques can be used with the First() function to get the first item in the collection.
If you don't know yet if you have any items in the collection you must check for NULLs or the calculation might fail:
import System static def GetAttributeValue(Incident): Value = Incident.Notes.Latest() if Value != null: return Value.Text else: return "No Notes!"
Getting the Minimum or Maximum value of a specific collection attribute
If you have a DateTime or number attribute (Decimal, Int16, or Int32) on a collection object and want to find the smallest or largest (older or newest in the case of DateTime) you can use the Min() and Max() functions. These work as follows:
import System static def GetAttributeValue(Incident): Smallest = Incident.Notes.Min("_MyInt") Largest = Incident.Notes.Max("_MyInt") return Largest
The value in quotes is the name of the attribute on the collection object you want to find the smallest or largest of, in this example _MyInt. As with the Latest() and First() functions you will need to check that the resulting value isn't NULL if you don't know there are any items in the collection yet.
Getting the number of items in a collection
If you want to know how many items there are in a collection this is very simple and uses the Count function:
import System static def GetAttributeValue(Incident): NumberOfNotes = Incident.Notes.Count return NumberOfNotes
Note that the Count function does not end with () like the others - this is because it is actually something called a Property and not a Method which the others are. You can use this as an alternative to checking if any items exist in a collection so you don't need to check for NULL values later on:
import System static def GetAttributeValue(Incident): if Incident.Notes.Count > 0: return Incident.Notes.Latest().Text else return 'No Notes!'
Looping through all items in a collection
If you want to access the data on a collection item that is not the first or last, or check something or add up something on all the collection items you can do this by looping through all the items in a collection. The syntax for a collection loop is as follows:
for Variable in Collection: Do something with Variable
You can name Variable anything and it is what you use within the loop to reference each item in the collection. In place of "Do something with Variable" you would write the code that you want to be executed for each item in the collection. This code should be indented further to show that it is the code used in the loop. The code is repeated for every item and once all items have been gone through the loop is complete and the calculation moves on to the whatever code is next.
Here is an example which simply counts the number of Notes on an Incident that were created by the Incident's Raise User:
import System static def GetAttributeValue(Incident): RaiseUserNotes = 0 for Note in Incident.Notes: if Note.CreationUser == Incident.RaiseUser: RaiseUserNotes++ return RaiseUserNotes
In this example we called the variable for the loop Note and then used this within the loop to reference the CreationUser attribute on the note. The line RaiseUserNotes++ increments the RaiseUserNotes variable by 1. You don't need to worry about checking if the collection has any items in bit before using a loop as the loop will quickly see that there aren't any and is therefore complete.
This example adds up a Working Time attribute on the Notes of an Incident. You would need to create an Int or Decimal attribute on the Note object and have the analysts enter a value for each Note.
import System static def GetAttributeValue(Incident): TotalWorkingTime = 0 for Note in Incident.Notes: TotalWorkingTime += Note._WorkingTime return TotalWorkingTime
The calculation first creates a variable called TotalWorkingTime and sets it to 0. The line TotalWorkingTime += Note._WorkingTime adds the value of the _WorkingTime attribute on the Note to the TotalWorkingTime variable.
Looping through a Collection in a specific order
In general the order a For loop processes the items is based on the sorting of the Default Query for that particular object. There are two exceptions to this though:
- When testing a calculation from the Calculation Editor using the Test Calculation button.
- When a BeforeSave calculation is triggered at the point a new item is added to the collection. The newly added item is always last despite the sorting on the query.
The second point can be an issue if you were assembling a summary of all collection items and wanted this in an order other than oldest to newest. For example if you want a list of all Notes on an Incident in newest to oldest order, you could set the Default Query for the Note object to sort by Creation Date or Serial Number in descending order and then use this calculation:
import System static def GetAttributeValue(Incident): Value = "Note Summary:" for Note in Incident.Notes: Value += String.Format("\r\n Note {0} add by {1}: {2}", Note.SerialNumber, Note.RaiseUser.Title, Note.Text) return Value
As you add the 3rd, 4th, 5th, etc. Note this will unfortunately list the Notes in reverse order until the last one added which will be at the end, for example: 3, 2, 1, 4.
To change this or any other ordering issue you can sort the collection by whatever attribute you want as part of the calculation itself:
import System static def GetAttributeValue(Incident): SortedNotes = List(Incident.Notes as Collections.IEnumerable) SortedNotes.Sort() do (first, last): return last.CreationDate.CompareTo(first.CreationDate) // this is newest to oldest order // return first.CreationDate.CompareTo(last.CreationDate) // this is commented out but is oldest to newest Value = "Note Summary:" for Note in SortedNotes: Value += String.Format("\r\n Note {0} add by {1}: {2}", Note.SerialNumber, Note.RaiseUser.Title, Note.Text) return Value
The first new line creates a List object using items from the Incident.Notes collection. The next two lines sort the list using a bit of code that might look a bit complicated but doesn't need much editing. The important part is last.CreationDate.CompareTo(first.CreationDate). You can change the sorting attribute by changing the two references to CreationDate to any DateTime, Number, or String attribute on the collection object. To change which order to sort in swap around the references to first and last. Changing to first.CreationDate.CompareTo(last.CreationDate) will sort oldest to newest instead.
Working with many-to-many linking objects
Many-to-many linking objects are used for collections such as parent/child linking and inter-module linking of processes. These need to be handled just like any other collection with a couple of important points to be aware of:
- You cannot use the First(), Latest(), Min(), or Max() functions. You must always use a loop.
- The collection itself takes you to the linking object record and from there is a one-to-one relationship to the record on the other end.
The following example loops through the children on an Incident and list the child IDs:
import System static def GetAttributeValue(Incident): Value = "No children" for ChildLink in Incident.Children: if Value == 'No children': Value = "Children ID(s): " + ChildLink.Child.Id else: Value += ", " + ChildLink.Child.Id return Value
The variable used in the loop is ChildLink which gets to the linking object record, then there is a one-to-one relationship called Child to get the the child Incident itself. Note that the order of the items in the collection might not be in any particular order.
In this example the variable Value gets set to 'No children' initially. If there aren't any children this is how the variable will be returned because there will be nothing to loop around. Within the loop the first "if" statement checks if the variable is still set to 'No children' - this will be the case on the first time around the loop so if this is the case the variable gets set to the text 'Children ID(s): ' and then the first ID. The 2nd, 3rd, 4th, etc. time around the loop the variable will instead be appended with a comma, a space, and the next ID. You can see it is being appended because it uses += instead of just = when setting the value.
If you know there will only be one item in the collection, for example accessing a parent Incident, you still need to loop through the collection. This example creates a variable called Parent which can be used later in the calculation:
import System static def GetAttributeValue(Incident): Parent = null for ParentLink in Incident.Parents: Parent = ParentLink.Parent if Parent != null: return "Parent ID: " + Parent.Id else: return "No Parent"
If there happened to be multiple parents in the collection the Parent variable will be overwritten with each iteration of the loop.