How Custom Functions are converted

This page describes how TrialGrid converts a C# Custom Function from the V1 to the V2 Medidata Rave API, for those who want to know what the 'Convert to V2' button and the "Convert to V2 API" action change, and why a function may not convert. Converting to the V2 API describes how to start a conversion, and how conversions are licensed.

The approach

TrialGrid makes only the changes V2 needs, and leaves everything else in the Source Code as it was, including its layout and comments.

  1. Rave says what to change. TrialGrid validates the Source Code with Medidata Designer. For each V1 reference, Medidata Rave reports the line and column it is on, the V1 type or member, and usually the V2 replacement. Medidata Rave has compiled the code, so it knows the type of every object, which the Source Code alone often doesn't show.

  2. |appname| makes the change. Where V2 keeps the same shape, the change is the name Medidata Rave suggests. Where the shape changes, for example an argument moves or a method becomes a property, a rule below makes it.

  3. Rave checks the result. The converted Source Code is validated again. Medidata Rave reports some V1 references only once those around them have changed, so if any remain the conversion is repeated, up to five times. A compile error from an ID that V2 gives as a reference, rather than a number, is repaired the same way: see below.

  4. The result is checked before it is saved. It must compile with no errors and no API Compatibility warnings, TrialGrid's own analysis must find no V1 left, and the function must use the same strings, call the same Custom Functions and perform the same actions as before. The arguments of each query, comment and sticky action are compared too.

If any step fails, the function is left unchanged and flagged with the reason.

Functions that call each other

Custom Functions linked by PerformCustomFunction, as caller or callee, directly or through others, are converted together or not at all, so a Draft never has a V2 function calling a V1 one or the reverse. A function that calls another by a name worked out when the code runs is treated as linked to every function its strings name. If one function in the group can't be converted, the others are flagged with its name.

A call keeps its arguments: CustomFunction.PerformCustomFunction(name, version, args) becomes Rave.PerformCustomFunction(name, version, args), and the called function reads its arguments as before.

Names and types

  • Types. Medidata Rave reports a V1 type once, so every use of it is changed: declarations, casts, generic arguments, arrays, typeof, as and is. A V1 type Medidata Rave doesn't report at all, as it may not in a cast, is changed to what Medidata Rave has suggested for it in other functions. A type written with its namespace, such as Medidata.Utilities.KeyValueCollection, is replaced whole. V1 types from Medidata.Core.Objects, Medidata.Utilities and other Medidata namespaces are all recognised.

  • The running check. ActionFunctionParams afp = (ActionFunctionParams)ThisObject; becomes IActionFunctionParams afp = Rave.Context.ActionFunctionParams;, and a member read through it, such as the action data point, becomes the matching Rave.Context property.

  • Renamed members take their V2 name, for example DataPoints on a record becomes AllDataPoints and FindByFieldOID becomes FindByFieldOid.

  • Static helpers moving to Rave, such as Number.IsValidInteger, become their Rave.* form with the same arguments.

  • Methods that become properties, such as dp.StandardValue(), lose their brackets.

  • IDs become references. x.ID becomes x.Ref where Medidata Rave suggests it, but only where the ID is just passed on, so that a reference serves as the number did: to a call whose V2 form takes a reference, to one of the function's own methods, into a variable that is itself only passed on, or compared for equality with another ID. A number declared from one, such as int v = subject.CRFVersionID;, is declared with var so it can hold the reference, and a variable, field, parameter or method's result declared int that carries one is declared as the reference type, which Medidata Rave names when it compiles the converted code. A function that does anything else with the ID is flagged: comparing it with 0, adding it to text, calling ToString() on it, storing or passing it as an object, or returning it as the function's result. A reference has no number to compare, and its text isn't the old number.

  • Documented replacements. Where Medidata documents a V2 replacement that Medidata Rave's warnings don't yet suggest, it is used: reading a user's Active becomes Enabled.

  • Freeze status. V2 has no freeze display status. dp.FreezeDisplayStatus == FreezeDisplayStatusEnum.Frozen becomes dp.IsFrozenRaw, and comparing with Unfrozen, or with !=, becomes !dp.IsFrozenRaw. V2's LockStatus isn't used: a data point frozen and then locked is still frozen to V1, and its LockStatus is Locked. Any other use of the V1 values, or of FreezeDisplayStatus itself, such as comparing it with a number or using it as text, is flagged.

Calls whose arguments change

  • Arguments follow their names. Where the V2 method takes the same arguments in a different order, they are matched by name, so CustomFunction.PerformQueryAction(text, group, ..., dp, ...) becomes Rave.Marking.PerformQueryAction(dp, text, group, ...). Where the names differ but the number is the same, they keep their positions.

  • A static call that becomes a method of its argument. CustomFunction.FetchAllDataPointsForOIDPath(field, form, folder, subject) becomes subject.GetDataPointsByOidPath(field, form, folder). V2 returns inactive data points too, so where V1 passed activeOnly as true the result is filtered to the active ones; false is simply dropped.

  • One method of several. Audits.Load(x) becomes x.FetchAudits(), the method Medidata Rave names on whichever type x is.

  • Doing or undoing. A method taking a Boolean that V2 splits in two, such as dp.Freeze(true) and dp.Freeze(false), becomes dp.Freeze() or dp.UnFreeze(); likewise Lock and Unlock. A variable chooses between them when the code runs.

  • Arguments V1 left out. V2 allows dp.Verify(isVerified) only with all three of its arguments written, so it becomes dp.Verify(isVerified, false, false).

  • Arguments passed with out or ref. A variable passed as out or ref is declared with the type the V2 method's parameter has, which must match it exactly.

Creating objects

  • Data pages and folders. new DataPage(instance, form, matrix), and instance.AddCRF(form, matrix), become instance.AddDataPage(form), which adds the page to the instance itself. Likewise new Instance(parent, folder, matrix) becomes parent.AddInstance(folder). The subject's V1 overloads add to the container they are given: subject.AddCRF(parent, form, matrix) becomes parent.AddDataPage(form), and subject.AddInstance(parent, folder, matrix) becomes parent.AddInstance(folder). The V1 DataPages.Add(page) or Instances.Add(instance) straight after is removed, since V2 has already added it, and so is one guarded only by if (page != null). V1's matrix isn't passed, since V2's AddDataPage(form) and AddInstance(folder) take none. This is done only where V1 passes a subject matrix, x.SubjectMatrixID read through any object: a page added that way is treated the same whichever one V1 passed. A function passing any other matrix, such as a matrix definition's ID, is flagged, because what dropping it changes hasn't been checked.

  • Key value pairs. list.Add(new KeyValue(key, value)) becomes list.Add(key, value). A variable that holds a pair only so it can be added goes: pair = new KeyValue(key, value); list.Add(pair); becomes list.Add(key, value);, and so does setting pair.Key and pair.Value on the lines before the Add. In the second form the pair must be created anew for each Add: V1 adds one pair whose values change as the same object each time, so the list ends up holding the last values more than once, and such a function is left for a person. V2 can't create a pair anywhere else. A pair kept in an ArrayList and only ever read back with ToString(), which gives its value, is replaced by the text: items.Add(new KeyValue(text, text)) becomes items.Add(text). This needs the key and the value to be the same, and every ArrayList in the function to stay in variables and methods declared ArrayList; any other pair kept as an object is flagged.

  • Collections. A V1 collection, such as DataPoints, becomes a read-only list in V2, as V2's methods return, even where Medidata Rave suggests a List. Where the function adds to, removes from or sorts one, it is declared as a List instead, holding a copy of what it is given. Variables declared together all become List when any of them is changed. new DataPoints() becomes new List<IDataPoint>(). A V2 method returning a sequence rather than a list, such as FilterDataPagesByFormOid, has .ToList() added so its result can still be counted and indexed.

  • Factories. A V1 constructor that V2 replaces with a factory, such as new KeyValueCollection(), becomes the factory call, here Rave.Edc.CreateKeyValueCollection().

Statements

  • Setting Active. V2 has no setter: x.Active = true; becomes x.Activate(null);, false becomes x.InActivate(null);, and a variable chooses between them when the code runs. Reading Active is unchanged.

  • Static helper methods. A function's own static helper methods could call V1's static methods, but they can't reach Rave. If any of them uses it after converting, none of the function's helper methods stays static.

What isn't converted

A function is flagged rather than converted when it uses:

  • something Medidata Rave says has no V2 equivalent, such as LibraryVersion.ID or setting a user's Active;

  • an endpoint Medidata Rave 2025.2.0 removed, or database access, which needs replacing with a stored procedure or another approach;

  • a V1 member or method whose V2 replacement has a different shape that no rule above covers.

It is also flagged when its source is longer than the 8,000 characters Medidata Rave takes: as it stands, or once converted, since V2's names are longer than V1's.

The flag gives the reason, and its detail lists each reference with its line.

What has been checked in a study

Some rules can't be shown correct by compiling. These have been checked by running a V1 and a V2 Custom Function side by side in a Medidata Rave study and comparing what each does:

  • AddDataPage(form) and AddInstance(folder) add the new page or folder where V1's constructors did, at the subject's level or inside a folder, and return it;

  • IsFrozenRaw gives the answer V1's freeze status comparison did for a data point that is frozen, unfrozen, locked, or frozen and then locked;

  • a page added to a folder is treated the same whichever subject matrix V1 passed: in a folder from a merged matrix, pages added with another object's subject matrix, with the folder's own and with V2's AddDataPage(form) all stay, active, when the matrix is unmerged;

  • GetDataPointsByOidPath returns what V1's FetchAllDataPointsForOIDPath did, inactive data points included, and filtering it to the active ones matches V1's activeOnly;

  • setting a record's Active and calling InActivate(null) or Activate(null) do the same, and an inactive record still holds its fields;

  • two references to the same data point are equal and two to different data points are not, as their IDs were;

  • Verify written with its two further arguments false verifies and unverifies a data point as V1's one-argument call does, including for a user whose role has no permission to verify: both verify the data point;

  • Freeze(), UnFreeze(), Lock() and Unlock() do what V1's Freeze and Lock did with true and false;

  • FetchAudits() returns the audits Audits.Load did, in the same order, newest first;

  • FilterDataPagesByFormOid(...).ToList() holds the pages V1's FilterDataPagesByFormOID did, in the same order;

  • a Custom Function called through Rave.PerformCustomFunction receives the arguments it was passed, as it did in V1;

  • a user's Enabled gives the same answer as V1's Active for an active user, and for an inactive one in Rave Classic, the only place an inactive user was compared;

  • a V1 KeyValue read with ToString() gives its value.

Converted Custom Functions should be tested like any other change before they are used in production.