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.
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.
|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.
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.
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,asandis. 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 asMedidata.Utilities.KeyValueCollection, is replaced whole. V1 types fromMedidata.Core.Objects,Medidata.Utilitiesand other Medidata namespaces are all recognised.The running check.
ActionFunctionParams afp = (ActionFunctionParams)ThisObject;becomesIActionFunctionParams afp = Rave.Context.ActionFunctionParams;, and a member read through it, such as the action data point, becomes the matchingRave.Contextproperty.Renamed members take their V2 name, for example
DataPointson a record becomesAllDataPointsandFindByFieldOIDbecomesFindByFieldOid.Static helpers moving to Rave, such as
Number.IsValidInteger, become theirRave.*form with the same arguments.Methods that become properties, such as
dp.StandardValue(), lose their brackets.IDs become references.
x.IDbecomesx.Refwhere 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 asint v = subject.CRFVersionID;, is declared withvarso it can hold the reference, and a variable, field, parameter or method's result declaredintthat 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, callingToString()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
ActivebecomesEnabled.Freeze status. V2 has no freeze display status.
dp.FreezeDisplayStatus == FreezeDisplayStatusEnum.Frozenbecomesdp.IsFrozenRaw, and comparing withUnfrozen, or with!=, becomes!dp.IsFrozenRaw. V2'sLockStatusisn't used: a data point frozen and then locked is still frozen to V1, and itsLockStatusisLocked. Any other use of the V1 values, or ofFreezeDisplayStatusitself, 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, ...)becomesRave.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)becomessubject.GetDataPointsByOidPath(field, form, folder). V2 returns inactive data points too, so where V1 passedactiveOnlyastruethe result is filtered to the active ones;falseis simply dropped.One method of several.
Audits.Load(x)becomesx.FetchAudits(), the method Medidata Rave names on whichever typexis.Doing or undoing. A method taking a Boolean that V2 splits in two, such as
dp.Freeze(true)anddp.Freeze(false), becomesdp.Freeze()ordp.UnFreeze(); likewiseLockandUnlock. 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 becomesdp.Verify(isVerified, false, false).Arguments passed with out or ref. A variable passed as
outorrefis 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), andinstance.AddCRF(form, matrix), becomeinstance.AddDataPage(form), which adds the page to the instance itself. Likewisenew Instance(parent, folder, matrix)becomesparent.AddInstance(folder). The subject's V1 overloads add to the container they are given:subject.AddCRF(parent, form, matrix)becomesparent.AddDataPage(form), andsubject.AddInstance(parent, folder, matrix)becomesparent.AddInstance(folder). The V1DataPages.Add(page)orInstances.Add(instance)straight after is removed, since V2 has already added it, and so is one guarded only byif (page != null). V1's matrix isn't passed, since V2'sAddDataPage(form)andAddInstance(folder)take none. This is done only where V1 passes a subject matrix,x.SubjectMatrixIDread 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'sID, is flagged, because what dropping it changes hasn't been checked.Key value pairs.
list.Add(new KeyValue(key, value))becomeslist.Add(key, value). A variable that holds a pair only so it can be added goes:pair = new KeyValue(key, value); list.Add(pair);becomeslist.Add(key, value);, and so does settingpair.Keyandpair.Valueon the lines before theAdd. In the second form the pair must be created anew for eachAdd: 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 anArrayListand only ever read back withToString(), which gives its value, is replaced by the text:items.Add(new KeyValue(text, text))becomesitems.Add(text). This needs the key and the value to be the same, and everyArrayListin the function to stay in variables and methods declaredArrayList; 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 aList. Where the function adds to, removes from or sorts one, it is declared as aListinstead, holding a copy of what it is given. Variables declared together all becomeListwhen any of them is changed.new DataPoints()becomesnew List<IDataPoint>(). A V2 method returning a sequence rather than a list, such asFilterDataPagesByFormOid, 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, hereRave.Edc.CreateKeyValueCollection().
Statements
Setting Active. V2 has no setter:
x.Active = true;becomesx.Activate(null);,falsebecomesx.InActivate(null);, and a variable chooses between them when the code runs. ReadingActiveis 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.IDor setting a user'sActive;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)andAddInstance(folder)add the new page or folder where V1's constructors did, at the subject's level or inside a folder, and return it;IsFrozenRawgives 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;GetDataPointsByOidPathreturns what V1'sFetchAllDataPointsForOIDPathdid, inactive data points included, and filtering it to the active ones matches V1'sactiveOnly;setting a record's
Activeand callingInActivate(null)orActivate(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;
Verifywritten with its two further argumentsfalseverifies 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()andUnlock()do what V1'sFreezeandLockdid withtrueandfalse;FetchAudits()returns the auditsAudits.Loaddid, in the same order, newest first;FilterDataPagesByFormOid(...).ToList()holds the pages V1'sFilterDataPagesByFormOIDdid, in the same order;a Custom Function called through
Rave.PerformCustomFunctionreceives the arguments it was passed, as it did in V1;a user's
Enabledgives the same answer as V1'sActivefor an active user, and for an inactive one in Rave Classic, the only place an inactive user was compared;a V1
KeyValueread withToString()gives its value.
Converted Custom Functions should be tested like any other change before they are used in production.