Timothy Leavitt · Jun 16, 2016 go to post

Your last example is really close - probably just the typo in the component ID that's the problem.

Class DC.ZenRadioOnLoad Extends %ZEN.Component.page
{

/// This XML block defines the contents of this page.
XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" title="">
<radioSet id="appRadio" name="appRadio" 
displayList="App One,App Two,App Three" 
valueList="One,Two,Three"
/>
</page>
}

/// This callback is called after the server-side page 
/// object and all of its children are created.<br/>
/// Subclasses can override this to add, remove, or modify 
/// items within the page object model, or to provide values
/// for controls.
Method %OnAfterCreatePage() As %Status
{
    Set ..%GetComponentById("appRadio").value = "Three"
    Quit $$$OK
}

}
Timothy Leavitt · Jun 17, 2016 go to post

The business operation's "Archive I/O" setting might do what you want, depending on what messages you're passing around. This will add some extra things to the message trace showing what the input to services or the output from operations is.

You can enable I/O archiving in the business operation's settings on the production configuration page, at the end of the "Development and Debugging" section.

Timothy Leavitt · Jul 10, 2016 go to post

This is a good use case for HAVING. Using Sample.Person as an example, the following queries are equivalent:

SELECT a.Age,A.Name
FROM 
   Sample.Person a
   LEFT JOIN Sample.Person b
   on a.home_state = b.home_state
     and a.DOB < b.DOB
WHERE
   b.ID is null

Implemented with HAVING:

SELECT Age,Name
FROM Sample.Person
GROUP BY Home_State
HAVING Age = MIN(Age)

The second is much more intuitive, and runs significantly faster too.

I suspect your query would be expressed as:

SELECT *
FROM client_address
GROUP BY client_id
HAVING date_updated = MIN(date_updated)
Timothy Leavitt · Jul 14, 2016 go to post

Another option might be "View Other":

If ClassA generates ClassB and sets GeneratedBy = ClassA.CLS, then ClassB will show up under "View Other" for ClassA. (Studio works the same way.)

Timothy Leavitt · Jul 21, 2016 go to post

Here's some documentation that might be helpful (if you're looking for a better solution): Packaging DeepSee Elements into Classes

It's also possible to export .DFI files directly with $System.OBJ.Export and reload with $System.OBJ.Load, same as with classes/routines.

Back to your actual question - what do you mean by "source code of a dashboard in udl format"?

If you mean a file containing the same XML you see in Studio - e.g.:

<?xml version="1.0"?><pivot xmlns="http://www.intersystems.com/deepsee/library" name="test2" folderName="" title="" description="" keywords="" owner="" shared="true" public="false" locked="false" resource="" timeCreated="2016-07-21T12:43:26.593Z" createdBy="tleavitt" category="" bookCover="" mdx="" cellWidth="120" columnHeaderStyle="" rowHeaderStyle="" cellStyle="" rowLabelSpan="true" columnLabelSpan="true" cellHeight="22" showEmptyRows="false" showEmptyColumns="false" cubeName="" caption="" listing="" listingRows="" showStatus="true" pageSize="100" colorScale="" rowTotals="false" columnTotals="false" rowTotalAgg="sum" columnTotalAgg="sum" rowTotalSource="page" showZebra="false" showRowCaption="true" printTitle="" printSubtitle="" printSubtitleOn="" showUser="" printPageSize="" printOrientation="" printMarginTop="" printMarginLeft="" printMarginRight="" printMarginBottom="" printLabelWidth="" printCellWidth="" autoExecute="true" manualMode="false" userMDX="" chartMarginTop="" chartMarginLeft="" chartMarginRight="" chartMarginBottom="" maxRows="" borderLeftCell="" borderRightCell="" borderTopCell="" borderBottomCell="" borderLeftCol="" borderRightCol="" borderTopCol="" borderBottomCol="" borderLeftRow="" borderRightRow="" borderTopRow="" borderBottomRow="" fontFamilyCell="" fontSizeCell="" fontFamilyCol="" fontSizeCol="" fontFamilyRow="" fontSizeRow="" showFilters="" showListingFilters="" showDate="" listingFontSize="" showZebraStripes="" filterTableStyle="" filterTableCaptionStyle="" filterTableItemStyle="" nowDisplayFormat="" measureLocation="" hideMeasures="" backgroundImage="" backgroundOpacity=".12"><rowAxisOptions spec="" key="" value="" text="" headEnabled="true" headCount="" filterEnabled="true" filterExpression="" orderEnabled="false" orderExpression="" orderDirection="BDESC" aggEnabled="false" aggFunction="" aggFunctionParm="" levelCaption="" levelFormat="" levelSummary="" levelType="" drillLevel="0" advanced="false" levelStyle="" levelHeaderStyle="" suppress8020="false" drilldownSpec="" enabled="true"></rowAxisOptions><columnAxisOptions spec="" key="" value="" text="" headEnabled="true" headCount="" filterEnabled="true" filterExpression="" orderEnabled="false" orderExpression="" orderDirection="BDESC" aggEnabled="false" aggFunction="" aggFunctionParm="" levelCaption="" levelFormat="" levelSummary="" levelType="" drillLevel="0" advanced="false" levelStyle="" levelHeaderStyle="" suppress8020="false" drilldownSpec="" enabled="true"></columnAxisOptions></pivot>

You could import it using %DeepSee.UI.FolderItemDocument:

Class User.DFIImport
{

/// Example use:
/// Do ##class(User.DFIImport).ImportDFIXML("foldername-itemname.pivot.DFI","C:\Temp\somefile.xml")
ClassMethod ImportDFIXML(pName As %String, pFilename As %String) As %Status
{
    Set tSC = $$$OK
    Try {
        set tStream = ##class(%Stream.FileCharacter).%New()
        set tSC = tStream.LinkToFile(pFilename)
        If $$$ISERR(tSC) { Quit }
        set tDoc = ##class(%DeepSee.UI.FolderItemDocument).%New(pName)
        set tSC = tDoc.ImportFromXML(tStream)
        If $$$ISERR(tSC) { Quit }
        set tSC = tDoc.Save()
        If $$$ISERR(tSC) { Quit }
    } Catch e {
        Set tSC = e.AsStatus()
    }
    Quit tSC
}

}

Note that the name specified for the %DeepSee.UI.FolderItemDocument will overwrite the "name" and "folderName" in the XML.

Timothy Leavitt · Aug 11, 2016 go to post

As written, the code in your post should work fine.

If the parameter '1' is actually a COS variable name, then the "undefined" would be expected (since the variable with that name is not defined in JavaScript), and you could use something like:

&js<zenPage.doReturn(#(..QuoteJS(myVariableName))#);>

Timothy Leavitt · Aug 16, 2016 go to post

If you're talking about storing dynamic data along with the session, see the documentation on %session.Data. This is just a multidimensional property - so you can do something like:

Set %session.Data("hello","world")=15

And then reference that data in a request later in the same session.

For a more advanced approach - for example, if there's a large amount of data related to the session - you could use one or more tables instead, and clear data when appropriate by implementing OnEndSession in a subclass of %CSP.SessionEvents and configuring that class as the session events class for your web application (in the web application's security settings).

Timothy Leavitt · Aug 22, 2016 go to post

You can pass a stream to $fromJSON instead of a string:

USER>set tStream = ##class(%Stream.TmpCharacter).%New()
 
USER>d tStream.Write("{""a"":2}")
 
USER>s obj = {}.$fromJSON(tStream)
 
USER>w obj.a
2

In your case:

Set RequestObj = ##class(%Object).$fromJSON(%request.Content)

This is much easier than reading the stream into a string first, and avoids <MAXLENTH> issues.

Timothy Leavitt · Sep 1, 2016 go to post

You can change namespace in Caché ObjectScript with:

Set $Namespace = "SomeOtherNamespace"

In methods that do that, it's typically good to start out with:

New $Namespace

To ensure that the namespace switches back once it's out of that context. Of course, you need to be sure that any code you call is available in the new namespace, and it would be good to think carefully about security implications.

The management portal uses the $NAMESPACE parameter in the URL along with %request.Context to add it to other URLs so you stay in the same namespace across pages.

Timothy Leavitt · Sep 20, 2016 go to post

I've observed the same issue (garbage output) on a few occasions when there is output (i.e., write statements) before HTTP headers are written. The garbage output might be a CSP Gateway issue, but it is wrong to write prior to headers anyway.

Other than redesigning the class entirely, one thing to try might be outputting headers at the beginning of OnHTTPHeader: 

Set tStatus = %response.WriteHTTPHeader(.OutputBody)

It looks like this doesn't happen automatically if OnHTTPHeader is overridden. Note that %response.WriteHTTPHeader(.OutputBody) will indicate "don't call OnPage" (OutputBody = 0) if there's a redirect or server-side redirect. It's worth considering how your custom OnHTTPHeader behavior should interact with redirects.

Timothy Leavitt · Oct 18, 2016 go to post

Here's one option:

<column header="Total Phones" colName="TotalPhones" colExpression="(select count(*) from ZenTutorial.PhoneNumber where Contact=ZenTutorial.Contact.ID)" width="9%" /> 

The colExpression property allows you to specify a more complex expression to get the value for the column.

Timothy Leavitt · Oct 20, 2016 go to post

The best tool I've seen for tracking unit test coverage in Caché ObjectScript is https://github.com/litesolutions/cache-utcov - maybe you'd already found that from some old Developer Community posts. However, it's gone stale, it's not really mature/complete, and it doesn't have a solution for the problem you've described. There are further complications because %Monitor.System.LineByLine looks at the generated (.int) code, which contains code that isn't in the class definition (because it's generated) and may not contain code that is in the class (for example, a classmethod that returns a constant). It also looks at code line-by-line, and there may be multiple statements on a line; tracing with ZBREAK instead could be a solution for this.

It's worth noting that #; comments don't appear in .int code - so, if only this type of comment is used, you could accurately measure the percentage of code coverage for a method/classmethod as the percentage of code coverage of the generated .int code corresponding to the method/classmethod. Otherwise, you're stuck parsing the code (which, if you're just trying to detect comments, wouldn't be too bad) to detect lines that contain comments and omit them from consideration when determining code coverage percentage.

Timothy Leavitt · Oct 26, 2016 go to post

Assuming you're doing this using the Studio extension framework, the class reference for %Studio.Extension.Base has really helpful documentation.

I think the pattern you want is:

In UserAction, return Action = 7 for your menu item(s):

7 - Display a dialog with a textbox and Yes/No/Cancel buttons. The text for this dialog is provided by the 'Target' return argument. The initial text for the textbox is provided by the 'Msg' return argument

The user can enter a password in this dialog. Then, in AfterUserAction, you can handle the user's response.

Timothy Leavitt · Nov 2, 2016 go to post

I think this is the expected/designed behavior - when you log in as that user, that routine runs rather than giving you the typical terminal prompt. When the routine quits/ends, so does the terminal session.

Timothy Leavitt · Nov 4, 2016 go to post

Here's one lazy option:

USER>w $System.SQL.CEILING(.1)
1
USER>w $System.SQL.CEILING(1.2)
2
USER>w $System.SQL.CEILING(1.7)
2

If you look at the implementation of that method in %SYSTEM.SQL, you'll see:

$s('$isvalidnum(val):"",+val[".":+$p(val,".")+(val>0),1:+val)

So that's another option (although messier).

Timothy Leavitt · Nov 4, 2016 go to post

There isn't a built-in method that does exactly what you're looking for, that I know of. Here's a simple example of how to do it, though:

Class DC.Demo.ArrayUtils
{

ClassMethod ArrayObjectToArray(pSource As %Collection.AbstractArray, Output pTarget)
{
    Kill pTarget
    
    Set tKey = ""
    For {
        Set tItem = pSource.GetNext(.tKey)
        Quit:tKey=""
        Set pTarget(tKey) = tItem
    }
}

ClassMethod ArrayToArrayObject(ByRef pSource, pTarget As %Collection.AbstractArray)
{
    // Could initialize pTarget here and return it Output, or return it normally.
    // This is just a bit more general because it'll work with any array collection type.
    Do pTarget.Clear()
    
    Set tKey = ""
    For {
        Set tKey = $Order(pSource(tKey),1,tItem)
        Quit:tKey=""
        Do pTarget.SetAt(tItem,tKey)
    }
}

}

Usage:

USER>s aodt=##class(%ArrayOfDataTypes).%New()
 
USER>w aodt.SetAt("lcavanaugh","username")
1
USER>w aodt.SetAt("organization","coolcompany")
1
USER>zw aodt
aodt=<OBJECT REFERENCE>[1@%Library.ArrayOfDataTypes]
+----------------- general information ---------------
|      oref value: 1
|      class name: %Library.ArrayOfDataTypes
| reference count: 2
+----------------- attribute values ------------------
|Data("coolcompany") = "organization"
|   Data("username") = "lcavanaugh"
|        ElementType = "%String"
+-----------------------------------------------------
 
USER>do ##class(DC.Demo.ArrayUtils).ArrayObjectToArray(aodt,.array)
 
USER>zw array
array("coolcompany")="organization"
array("username")="lcavanaugh"
 
USER>s newaodt = ##class(%Library.ArrayOfDataTypes).%New()
 
USER>do ##class(DC.Demo.ArrayUtils).ArrayToArrayObject(.array,newaodt)
 
USER>zw newaodt
newaodt=<OBJECT REFERENCE>[2@%Library.ArrayOfDataTypes]
+----------------- general information ---------------
|      oref value: 2
|      class name: %Library.ArrayOfDataTypes
| reference count: 2
+----------------- attribute values ------------------
|Data("coolcompany") = "organization"
|   Data("username") = "lcavanaugh"
|        ElementType = "%String"
+-----------------------------------------------------

If you really wanted to make BuildValueArray work:

USER>d aodt.%SerializeObject(.serial)
 
USER>d aodt.BuildValueArray($lg(serial),.anotherarray)
 
USER>zw anotherarray
anotherarray("coolcompany")="organization"
anotherarray("username")="lcavanaugh"

The utility method approach would probably at least be clearer. smiley

Timothy Leavitt · Nov 7, 2016 go to post

There actually is a way to get this information that should work in many cases, using %Studio.Debugger:SourceLine, but it's not really easy. Note that %Studio.Debugger comes with the warning:

This class is used internally by Caché. You should not make direct use of it within your applications. There is no guarantee made about either the behavior or future operation of this class.

That said, here's a quick sample:

Class DC.Demo.SourceLine
{

ClassMethod Run()
{
    #; Here are a few comments to screw things up.
    Try {
        #; More comments!
        Write "Hello world",!
        Write 1/0
    } Catch e {
        Do ..HandleException(e)
    }
}

ClassMethod HandleException(pException As %Exception.AbstractException)
{
    Write "Exception occurred: ",pException.DisplayString(),!
    
    //Example value if e.Location (from above error): zRun+3^DC.Demo.SourceLine.1
    Set tSC = ..GetClassSourceLine(pException.Location,.tClsLocation)
    If $$$ISERR(tSC) {
        Write $System.Status.GetErrorText(tSC),!
    } else {
        Write ".INT error location: "_pException.Location,!
        Write ".CLS error location: "_tClsLocation,!
    }
}

ClassMethod GetClassSourceLine(pIntLocation As %String, Output pClsLocation As %String) As %Status
{
    Set tStatus = $$$OK
    Set pClsLocation = ""
    Try {
        Set tMethodAndLine = $Piece(pIntLocation,"^",1)
        Set tIntName = $Piece(pIntLocation,"^",2)
        Set tTag = $Piece(tMethodAndLine,"+")
        Set tRelativeOffset = $Piece(tMethodAndLine,"+",2)
        
        // Get routine text to find the absolute offset of tTag
        Set tTagOffset = 0
        Set tEndPos = 0
        Set tTextLines = 0
        For {
            Set tLine = $Text(@("+"_$Increment(tTextLines)_"^"_tIntName))
            Quit:tLine=""
            
            // Example:
            // zRun() public {
            // This relies on an assumption that methods will be sorted alphabetically and won't contain labels.
            If $Extract(tLine,1,$Length(tTag)) = tTag {
                Set tTagOffset = tTextLines //tTextLines is the counter.
                Set tEndPos = $Length(tLine)
                Quit
            }
        }
        
        // The absolute offset of the line in the .int file is the tag's offset plus the offset within it.
        Set tOffset = tTagOffset + tRelativeOffset
        Set tStatus = ##class(%Studio.Debugger).SourceLine(tIntName,tOffset,0,tOffset,tEndPos,,.tMap)
        If $$$ISERR(tStatus) {
            Quit
        }
        If $Data(tMap("CLS",1)) {
            Set $ListBuild(tClass,tMethod,tLine,tEndPos,tNamespace) = tMap("CLS",1)
            Set pClsLocation = tClass_":"_tMethod_"+"_tLine
        }
    } Catch e {
        Set tStatus = e.AsStatus()
    }
    Quit tStatus
}

}

And sample output:

USER>d ##class(DC.Demo.SourceLine).Run()
Hello world
Exception occurred: <DIVIDE> 18 zRun+3^DC.Demo.SourceLine.1
.INT error location: zRun+3^DC.Demo.SourceLine.1
.CLS error location: DC.Demo.SourceLine:Run+5

If an exception occurs in generated code, I'd expect this to be useless.

Timothy Leavitt · Nov 15, 2016 go to post

Certain configuration steps must be taken to enable access to % classes in non-/csp/sys/... web applications. Here's a link to the documentation about that.

Also, rather than creating your own % classes, you might consider either using the %All namespace (see documentation) or starting the package name with %Z. Starting the class's package name with %Z will prevent the class from being overwritten upon upgrade (which is important!) and, as a bonus, would also allow access from non-/csp/sys/ web applications automatically (according to the documentation at the first link).

Timothy Leavitt · Nov 17, 2016 go to post

I think these error codes correspond to Windows system error codes - see this page.

So -2 corresponds to "ERROR_FILE_NOT_FOUND" and -3 to "ERROR_PATH_NOT_FOUND"

It seems more likely that you would get -2 from ##class(%File).Delete(file,.return). passing a directory that simply doesn't exist to RemoveDirectory results in -3. Is RemoveDirectory actually returning that error code, or perhaps a different one?

Timothy Leavitt · Nov 17, 2016 go to post

Hi Sebastian,

Sorry this went unanswered for so long. In case you haven't figured it out yet, one solution is:

    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)

%AddComponent registers the parameter with the page, so you won't get that error. (There might be a better way to do this, but %AddComponent at least seems to work.)

Here's a full example:

Class DC.Demo.DynamicComboPage Extends %ZEN.Component.page
{

Property count As %ZEN.Datatype.integer [ InitialExpression = 0 ];

/// This XML block defines the contents of this page.
XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" title="">
<button onclick="zenPage.AddCombo();" caption="Add a Combo Box!" />
</page>
}

Method AddCombo() [ ZenMethod ]
{
    #dim %page As DC.Demo.DynamicComboPage
    Set ..count = ..count + 1
    Set tParm = ##class(%ZEN.Auxiliary.parameter).%New()
    Set tParm.id = "parameter_"_..count
    Set tParm.name = "parameter_"_..count

    Set tExam = ##class(%ZEN.Component.dataCombo).%New()
    Set tExam.id = "dataCombo_"_..count
    Set tExam.label = "Test"
    
    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)
}

}
Timothy Leavitt · Nov 28, 2016 go to post

The Zen equivalent of ##super for JS is invokeSuper. Typically you'd just use:

this.invokeSuper('nameOfMethod',arguments);

All Zen components (including pages) have this. Here's an example from %ZEN.Component.dataCombo:

///  Set the value of a named property.<br>
ClientMethod setProperty(property, value, value2) [ Language = javascript ]
{
    switch(property) {
    case 'itemCount':
        break;
    case 'parameters':
        // set value of parameter: note that value will
        // be 1-based, so we have to convert it.
        // changing parameter always forces a query execution,
        // even in snapshot mode.
        if ('' != value) {
            value = value - 1;
            if (this.parameters[value]) {
                if (this.parameters[value].value != value2) {
                    this.parameters[value].value = value2;
                    this.clearCache();
                }
            }
        }
        break;
    default:
        // dispatch
        return this.invokeSuper('setProperty',arguments);
        break;
    }

    return true;
}
Timothy Leavitt · Nov 30, 2016 go to post

Another option to consider, if you have the flexibility to do so, would be putting an [IDKey] index on the code property of the code tables, having the code properties in ICDAutoCodeDefn refer to the code table classes rather than being of type %String, and then using implicit joins. I suspect (but haven't verified) that this would perform better than calculated/transient properties, and it's much easier to follow/maintain than normal JOINs.

Here's a full example. Looking at a general code table class:

Class DC.Demo.CodeTables.CodeTable Extends %Persistent [ Abstract, NoExtent ]
{
Index Code On Code [ IdKey ];
Property Code As %String;
Property Description As %String;
}

In this example, a specific code table would then extend that class, but don't need to add anything:

Class DC.Demo.CodeTables.Team Extends DC.Demo.CodeTables.CodeTable
{
}

Class DC.Demo.CodeTables.Position Extends DC.Demo.CodeTables.CodeTable
{
}

Then, in the class that refers to these code tables:

Class DC.Demo.CodeTables.Players Extends %Persistent
{
Property Name As %String(MAXLEN = 100);
Property Position As DC.Demo.CodeTables.Position;
Property Team As DC.Demo.CodeTables.Team;
ForeignKey Position(Position) References DC.Demo.CodeTables.Position();
ForeignKey Team(Team) References DC.Demo.CodeTables.Team();
}

(It's worth considering using foreign keys in cases like this.)

To demonstrate how this ends up working from an SQL perspective:

Class DC.Demo.CodeTables.Driver
{

ClassMethod Run()
{
    Do ##class(DC.Demo.CodeTables.Players).%KillExtent()
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')")
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')")
    Do ..ShowQueryResults("insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')")
    Do ..ShowQueryResults("select Name,Position->Description ""Position"",Team->Description ""Team"" from DC_Demo_CodeTables.Players")
}

ClassMethod ShowQueryResults(pQuery As %String, pParams...)
{
    Write !,"Running query: ",pQuery,!
    Do ##class(%SQL.Statement).%ExecDirect(,pQuery,pParams...).%Display()
}

}

The output from the Run() method is:

USER>Do ##class(DC.Demo.CodeTables.Driver).Run()

Running query: insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')
1 Row Affected
Running query: insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')
1 Row Affected
Running query: insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')
1 Row Affected
Running query: select Name,Position->Description "Position",Team->Description "Team" from DC_Demo_CodeTables.Players
Name    Position    Team
Rob Gronkowski    Tight End    New England Patriots

1 Rows(s) Affected
Timothy Leavitt · Dec 7, 2016 go to post

It's defined in %cspInclude.inc as:

#define URLENCODE(%string) $zconvert($zconvert(%string,"O",$replace($$$GETIO,"JSML","UTF8")),"O","URL")

See documentation on $ZCONVERT as well. This mentions:

“UTF8” which converts (output mode) 16-bit Unicode characters to a series of 8-bit characters. Thus, the characters $CHAR(0) through $CHAR(127) are the same in RAW and UTF8 mode; characters $CHAR(128) and above are converted.

“URL” which adds (output mode) or removes (input mode) URL parameter escape characters to a string. Characters higher than $CHAR(255) are represented in Unicode hexadecimal notation: $CHAR(256) = %u0100.

So, in short, $$$URLENCODE converts the input from (possibly) 16-bit to 8-bit characters, then adds URL parameter escape characters where they're needed. (For example, escaping slashes.)