Timothy Leavitt · Jul 30, 2018 go to post

This isn't the answer to your actual question, but it's worth pointing out ##class(%Library.File).ManagerDirectory()  - instead of referencing the global, you could use:

##class(%Library.File).ManagerDirectory()_"LDAPKeyStore/"

Also, ##class(%SYS.System).GetInstanceName() returns the instance name; you shouldn't need to set that in a global, and (furthermore) the instance name isn't necessarily part of the path to the mgr directory (the two can be configured independently).

Other than that I think you'd be stuck with using XECUTE or $Xecute to do what you originally suggested.

Timothy Leavitt · Jul 12, 2018 go to post

After further review, I'm really not sure why the first/second queries don't use the index. The problem with the last query is that %Key is the index in the list, not anything about Tag itself.

Here's a solution that performs well in my testing:

Class DC.Demo.Tag Extends (%Persistent, %Populate)
{

Index Tag On Tag [ Unique ];

Property Tag As %String;

}

Class DC.Demo.Tagged Extends (%Persistent, %Populate)
{

Relationship HasTags As DC.Demo.HasTag [ Cardinality = children, Inverse = Tagged ];

ClassMethod Run()
{
    Do ..%KillExtent()
    Do ##class(DC.Demo.Tag).%KillExtent()
    
    Do ##class(DC.Demo.Tag).Populate(50)
    Do ..Populate(5000)
    Do ##class(DC.Demo.HasTag).Populate(10000)
}

}

Class DC.Demo.HasTag Extends (%Persistent, %Populate)
{

Relationship Tagged As DC.Demo.Tagged [ Cardinality = parent, Inverse = HasTags ];

Property Tag As DC.Demo.Tag [ Required ];

Index UniqueTag On Tag [ IdKey ];

Index TaggedByTag On (Tag, Tagged);

}
Timothy Leavitt · Jun 13, 2018 go to post

It's not the prettiest, but I think the simplest solution would be to avoid navigating to the parent object entirely:

  • Add a Foobar property to EmbedObj.
  • Via a row/object trigger in ContainerObj, propagate changes to Foobar to EmbedObj_Foobar.
    • As an initial step for data population in existing records, run SQL: update ContainerObj set EmbedObj_Foobar = Foobar
  • Base your SQLComputeCode on the copy of Foobar in the serial class.
Timothy Leavitt · Jun 12, 2018 go to post

Re: extending method keywords, you can't do that at this time, but a useful approximation is structuring a comment - for example:

/// @MyKeyword MyValue
ClassMethod TestOne()
{
    // Implementation
}

And then looking at the %Dictionary.MethodDefinition / %Dictionary.CompiledMethod documentation in a generator method. (But it looks like you might already be on to that with @AutoGenerated.)

Re: making first compilation work, this works for me, by making the first compilation automatically trigger a second one when needed:

ClassMethod GenerateMethods() As %Status [ CodeMode = objectgenerator ]
{
    For i=1:1:%class.Methods.Count() {
        #dim method As %Dictionary.MethodDefinition = %class.Methods.GetAt(i)
        Continue:((method.Description["@AutoGenerated") || (method.CodeMode="objectgenerator"))
        Do %class.Methods.Insert(##class(util.TestGenerator).Generate(method.Name_"DoSomethingElse",%class.Name))
    }
    If (%class.Methods.%IsModified()) {
        Do ##class(%Projection.AbstractProjection).QueueClass(%class.Name)
    }
    Quit %class.%Save()
}

Test code:

Class util.Driver
{

ClassMethod Run()
{
    do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestOneDoSomethingElse")
    do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestTwoDoSomethingElse")
    do $system.OBJ.UnCompile("util.*")
    do $system.OBJ.Compile("util.*","ck")
}

}
Timothy Leavitt · Jun 8, 2018 go to post

If you want to count (or otherwise traverse) all the elements in a multidimensional array, you can use $Query - here's a sample with method that does that:

ClassMethod Run()
{
    Set a(1) = "blue"
    Set a(1,3,5,2) = "navy"
    Set a(2) = "red"
    Set a(2,2) = "crimson"
    Set a(3) = "yellow"
    
    Write ..Count(.a)
}

ClassMethod Count(ByRef pArray) As %Integer [ PublicList = pArray ]
{
    Set count = 0
    Set ref = $Query(pArray(""))
    While (ref '= "") {
        Set count = count + 1
        Set ref = $Query(@ref)
    }
    Quit count
}
Timothy Leavitt · May 30, 2018 go to post

The key point here is how to get the value of the "DE" property parameter from the OPTREGISTER class definition into the "DE" property of the corresponding property. The naming does not do that automatically. Generally, property parameters (like DE, DICLookUp, XMLNAME) are used for code generation, either in methods (the third example below), projections (like XML), or for validation of datatypes (e.g., %String MINLEN/MAXLEN).

Assuming XML export is done with (for example):

Set = ##class(Test.SendExternalModel.OPTREGISTER).%New()
Do o.XMLExport(,",literal,indent")

There are at least three options. I'd recommend the third, but the first two are perhaps easier to understand.

1) In a method of OPTREGISTER (perhaps %OnNew), instantiate the various properties and set the DE property of each of them. In this case, the DE property parameter is no longer needed. For example:


Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
  Set ..THBZ = ##class(Test.SendExternalModel.DE2).%New()
  Set ..THBZ.DE = "DEX71.41.007.01"
  Set ..GHFS = ##class(Test.SendExternalModel.DE2).%New()
  Set ..GHFS.DE = "DEX71.41.008.01"
  Set ..GHF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..GHF.DE = "DEX71.41.009.01"
  Set ..ZLF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..ZLF.DE = "DEX71.41.009.01"
  Set ..QTF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..QTF.DE = "DEX71.41.009.01"
  Set ..WDBZ = ##class(Test.SendExternalModel.DE2).%New()
  Set ..WDBZ.DE = "DEX71.41.010.01"
  Set ..GHKSDM = ##class(Test.SendExternalModel.DE2).%New()
  Set ..GHKSDM.DE = "DE08.10.025.00"
  Quit $$$OK
}

2) Have DE1/DE2 extend %SerialObject rather than %RegisteredObject, and define a constructor in DE1. (DE2 can have inheritance simplified a bit, although perhaps not in the context of your full application.) Then, define properties in OPTREGISTER with an InitialExpression - this is passed to the constructor for the serial object. This approach may not actually make sense in the context of your full application.

PropertyParameters:

Class Test.Common.PropertyParameters Extends %XML.PropertyParameters [ ProcedureBlock ]
{

Parameter HDSD As STRING;

Parameter DE As STRING;

Parameter DICLookUp As STRING;

}

DE1:

Class Test.SendExternalModel.DE1 Extends (%SerialObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ]
{

Parameter XMLIGNOREINVALIDTAG = 1;

Parameter XMLIGNORENULL = 1;

Property DE As %String(XMLNAME = "de", XMLPROJECTION = "ATTRIBUTE");

Method %OnNew(pDE As %String = "") As %Status [ Private, ServerOnly = 1 ]
{
  Set ..DE = pDE
  Quit $$$OK
}

Storage Default
{
<Data name="DE1State">
<Value name="1">
<Value>DE</Value>
</Value>
</Data>
<State>DE1State</State>
<StreamLocation>^Test.SendExternalModel.DE1S</StreamLocation>
<Type>%Library.CacheSerialState</Type>
}

}

DE2:

Class Test.SendExternalModel.DE2 Extends Test.SendExternalModel.DE1
{

Property Display As %String(XMLNAME = "display", XMLPROJECTION = "ATTRIBUTE");

Storage Default
{
<Data name="DE2State">
<Value name="1">
<Value>DE</Value>
</Value>
<Value name="2">
<Value>Display</Value>
</Value>
</Data>
<State>DE2State</State>
<StreamLocation>^Test.SendExternalModel.DE2S</StreamLocation>
<Type>%Library.CacheSerialState</Type>
}

}

OPTREGISTER:

Class Test.SendExternalModel.OPTREGISTER Extends (%RegisteredObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ]
{

Parameter XMLIGNOREINVALIDTAG = 1;

Parameter XMLIGNORENULL = 1;

Parameter XMLNAME = "OPTREGISTER";

Property THBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.007.01", DICLookUp = "Y", XMLNAME = "THBZ") [ InitialExpression = "DEX71.41.007.01", Required ];

Property GHFS As Test.SendExternalModel.DE2(DE = "DEX71.41.008.01", DICLookUp = "Y", XMLNAME = "GHFS") [ InitialExpression = "DEX71.41.008.01", Required ];

Property GHF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "GHF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property ZLF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "ZLF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property QTF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "QTF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property WDBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.010.01", DICLookUp = "Y", XMLNAME = "WDBZ") [ InitialExpression = "DEX71.41.010.01" ];

Property GHKSDM As Test.SendExternalModel.DE2(DE = "DE08.10.025.00", DICLookUp = "Y", XMLNAME = "GHKSDM") [ InitialExpression = "DE08.10.025.00", Required ];

}

3) Given all of your original class definitions, add a generator method in OPTREGISTER (or a parent class) that sets the "DE" property of any properties of type DE1/DE2 (or perhaps any properties that have your custom property parameters class) based on the DE property parameter in the containing class's definition. Ensure this method is called prior to XML export. (Might be useful if you need more fine-grained control - i.e., omitting the WDBZ element if it has no content.) This would be a great approach if you have lots of similar classes like these, to avoid repeating (1) every time. A really simple implementation (without any extra sanity checking for property types, etc.), equivalent to the first example, would look like:

Method %OnNew() As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ]
{
  Set tKey = ""
  For {
    #dim tProperty As %Dictionary.CompiledProperty
    Set tProperty = %compiledclass.Properties.GetNext(.tKey)
    If (tKey = "") {
      Quit
    }
    Set tDE = tProperty.Parameters.GetAt("DE")
    If (tDE '= "") {
      Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_" = ##class("_tProperty.Type_").%New()")
      Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_".DE = "_$$$QUOTE(tDE))
    }
  }
  Do %code.WriteLine(" Quit $$$OK")
  Quit $$$OK
}
Timothy Leavitt · May 18, 2018 go to post

Thanks for pointing that out, but it's not really ideal for my use case, since it's not a % class and explicitly documented "Only to be used in the context of DataCheck." I think I'd also like to permit extended global references, which my original approach does.

Timothy Leavitt · May 18, 2018 go to post

In the possible absence of a built-in class for such a purpose, this seems to work:

Class DC.Demo.GlobalReference Extends %String [ ClassType = datatype ]
{

/// 511 is an upper bound for the maximum length of a global reference - see:
/// <a href="https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GGBL_structure#GGBL_structure_maxsubscrlen">Maximum Length of a Global Reference</a>
Parameter MAXLEN = 511;

ClassMethod IsValid(%val As %CacheString) As %Status [ ServerOnly = 0 ]
{
    Set tOldZReference = $ZReference
    Set tSC = $$$OK
    Try {
        Set $ZReference = %val
    } Catch e {
        // The above SET will throw a <SYNTAX> exception for an invalid global reference
        Set tSC = $$$ERROR($$$GeneralError,$$$FormatText("Invalid global reference: %1",%val))
    }
    Set $ZReference = tOldZReference
    Quit tSC
}

}
Timothy Leavitt · May 9, 2018 go to post

With a dynaForm, this would look like:

Class DC.Demo.ZenLocalizationModel Extends %ZEN.DataModel.ObjectDataModel
{

Property Descrição As %String(ZENATTRS = "requiredMessage:obrigatório.") [ Required ];

}

And:

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

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<dataController id="myController" modelClass="DC.Demo.ZenLocalizationModel" />
<dynaForm controllerId="myController" injectControls="before">
<submit caption="Enviar" />
</dynaForm>
</page>
}

}

For more information on how to customize data model behavior with property parameters, see the class reference for %ZEN.DataModel.objectModelParameters.

Timothy Leavitt · Apr 30, 2018 go to post

For completeness, here's an extension of the above example that actually works the way you'd expect (using PublicList to handle scope, and New to avoid leaking any variables):

Class DC.Demo.Indirection
{

ClassMethod Driver() [ PublicList = x ]
{
    New x
    Do ..Run()
    Do ..RunSets()
    Do ..Run()
}

ClassMethod Run() [ PublicList = x ]
{
    Set x = 5
    Try {
        Write @"x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
    Try {
        Xecute "write x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
}

ClassMethod RunSets() [ PublicList = x ]
{
    Set x = 5
    Set @"x" = 42
    Write x,!
    Xecute "set x = 42"
    Write x,!
}

}

Results:

USER>d ##class(DC.Demo.Indirection).Driver()
5
5
42
42
5
5
 
USER>w

USER>

But that doesn't mean it's a good idea to do things like this.

Timothy Leavitt · Apr 30, 2018 go to post

This can be done with the requiredMessage property of %ZEN.Component.control. There are two ways to accomplish this:

1. Just add the requiredMessage attribute

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

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<form>
<text label="Descrição" required="true" requiredMessage="obrigatório." />
<submit caption="Enviar" />
</form>
</page>
}

}

Problem is, you'd need to do that in a lot of different places. Instead, you could...

2. Use custom components that subclass built-in control types.

Sample component:

Class DC.Demo.Component.text Extends %ZEN.Component.text [ System = 3 ]
{

/// Feel free to customize this.
Parameter NAMESPACE = "https://community.intersystems.com/post/change-language-required";

/// Value displayed in alert box by the form <method>validate</method>
/// method when this control is required and does not have a value.<br>
/// This is a localized value.
Property requiredMessage As %ZEN.Datatype.caption [ InitialExpression = "obrigatório." ];

}

Sample page using component:

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

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" xmlns:custom="https://community.intersystems.com/post/change-language-required">
<form>
<custom:text label="Descrição" required="true" />
<submit caption="Enviar" />
</form>
</page>
}

}

I was hoping there would be a way to use Zen's localization features to do this, but it seems like that isn't an option, unfortunately;"required." is hard-coded as the InitialExpression for requiredMessage in %ZEN.Component.control, and may only be localized within a page using the component if a non-default value is specified.

Timothy Leavitt · Apr 30, 2018 go to post

Most of the time, you don't really need to use XECUTE/indirection/etc - there is usually a better way to accomplish the same thing. Furthermore, in modern coding practices, XECUTE and name indirection probably will not work the way you expect, because they can only access public variables (not those in the scope of your method). For example, given the following class:

Class DC.Demo.Indirection
{

ClassMethod Run()
{
    Set x = 5
    Try {
        Write @"x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
    Try {
        Xecute "write x"
    } Catch e {
        Write e.DisplayString()
    }
}

ClassMethod RunSets()
{
    Set x = 5
    Set @"x" = 42
    Write x,!
    Xecute "set x = 42"
    Write x,!
}

}

The output when run in terminal is:

USER>Do ##class(DC.Demo.Indirection).Run()
<UNDEFINED> 9 zRun+3^DC.Demo.Indirection.1 x
<UNDEFINED> 9 zRun+9^DC.Demo.Indirection.1 x
USER>d ##class(DC.Demo.Indirection).RunSets()
5
5
USER>d ##class(DC.Demo.Indirection).Run()
42
42

Your code certainly should not rely on such scoping behavior. There are ways around this (for example, adding  [ PublicList = x ] to the method signature, using New, etc.), but they are messy and difficult to understand and maintain.

In your particular case, using a local array would probably be a better approach - for example:

set x(counter) = DIAGS

Although if you provided more context for why you think you need to dynamically set variables, the community might have other suggestions.

Timothy Leavitt · Apr 27, 2018 go to post

There was an issue with database compaction in 2014.1.1 that could cause database degradation - see this alert: https://www.intersystems.com/support-learning/support/product-news-alerts/support-alert/alert-database-compaction/

Given that, I would recommend contacting the experts in the Worldwide Response Center (InterSystems Support) to help investigate. Running an integrity check on the database you compacted would be a good start - see https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_integrity#GCDI_integrity_verify_portal.

Timothy Leavitt · Mar 29, 2018 go to post

Another slightly more lightweight approach:

Class DC.Demo.combobox Extends %ZEN.Component.combobox [ System = 3 ]
{

Parameter NAMESPACE = "http://community.intersystems.com/demo";

/// Notification that this component is about to become modal.
ClientMethod onStartModalHandler(zindex) [ Language = javascript ]
{
    this.invokeSuper('onStartModalHandler',arguments);
    this.fixLinks(this.getDropDownDiv());
}

/// Look for all children of specified element, and change links with href="#" to also have onclick = "return false;"
ClientMethod fixLinks(element) [ Language = javascript ]
{
    for (var i = 0; i < element.children.length; i++) {
        this.fixLinks(element.children[i]);
    }
    if (element.getAttribute("href") == "#") {
        element.onclick = function() { return false; };
    }
}

}
Timothy Leavitt · Mar 28, 2018 go to post

We specifically chose not to support this capability in Atelier, other than for the special case of launching web pages. As an implementation of Studio Source Control, running an executable on the client only makes sense in cases where the "server" and the "client" are always the same, and for that model the source control plugins publicly and freely available in the Eclipse ecosystem are far superior in terms of support and usability.

I would be interested to hear more details of your use case. What version control system are you using? Git?

(Note to other readers: "UserAction = 3" and other details of the Studio Extension framework are documented here.)

Timothy Leavitt · Mar 20, 2018 go to post

What are you trying to do? This?

USER>Set rm=##class(%Regex.Matcher).%New("[0-9]","Word 123456")

USER>Write rm.ReplaceAll(".")
Word ......

Or:

USER>Set rm=##class(%Regex.Matcher).%New("[0-9]+","Word 123456")

USER>Write rm.ReplaceAll(".")
Word .
Timothy Leavitt · Feb 6, 2018 go to post
USER>d $System.OBJ.GetPackageList(.classes,"%CSP.Documatic") zw classes
classes("%CSP.Documatic.CubeInfo")=""
classes("%CSP.Documatic.PrintClass")=""
Timothy Leavitt · Dec 12, 2017 go to post

Thanks Eduard, this looks promising. I'll try it out and post any interesting results.

Timothy Leavitt · Nov 28, 2017 go to post

You could have your unit test class extend the class with the [Private] method, or (if that doesn't make sense or causes issues) write another class that your unit test loads that extends the class with the [Private] method and has another method that wraps it.

Timothy Leavitt · Nov 21, 2017 go to post

There are two good approaches to this, both relying on an onchange event handler on the first dataCombo.

The first, simpler solution would be to just have an onchange event handler set the value directly:

<dataCombo id="Grupo_Producto" name="Grupo_Producto" dataBinding="bdGrupo" size="40" 
 label="(*) Grupo" dropdownWidth="400" dropdownHeight="100" loadingMessage="Cargando..."
 columnHeaders="Codigo, Nombre"
 queryClass="Innova.CL.Core.BD.Configuracion.Operacional.Grupo" queryName="obtieneGrupoProd"
 sqlLookup="SELECT Nombre FROM Innova_CL_Core_BD_Configuracion_Operacional.Grupo Where ID = ?"
 onchange="zen('Linea_Producto').setProperty('parameters',1,zenThis.getValue());"
 > 
</dataCombo>

However, if you have multiple components on the page that depend on the current value of Grupo_Producto, it might be simpler to use a property of the page that has the value of the component copied to it when that value changes. This is handy because you can reference the value in a Zen expression, e.g., #(%page.GrupoProducto)#, rather than needing to copy the same value to lots of places. Here is an example with two dataCombos in the SAMPLES namespace:

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

Property homeState As %ZEN.Datatype.string;

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<dataCombo id="homeState" label="State"
sql="select distinct Home_State from Sample.Person order by Home_State"
onchange="zenPage.dataComboChanged(zenThis)"
/>
<dataCombo id="homeCity" label="City"
sql="select distinct %exact Home_City from Sample.Person where Home_State = ? order by Home_City">
<parameter value="#(%page.homeState)#" />
</dataCombo>
</page>
}

ClientMethod dataComboChanged(component) [ Language = javascript ]
{
    // Update the page property's value.
    // Note: page property and component ID don't necessarily need to match, but it's convenient if they do.
    // (Imagine a cascade of dataCombos with dependent values.)
    zenPage.setProperty(component.id,component.getValue());
    
    // Possibly also call refreshContents() on components referencing properties that may have changed,
    // and/or clear values that may have been invalidated by the change.
    // dataCombo does not need this, but other components (e.g., tablePane) would.
}

}
Timothy Leavitt · Sep 20, 2017 go to post

I can't speak to a fix, but a few thoughts:

  • There is a critical difference between Studio and Atelier projects: Atelier projects are not even present on the server / in the database. This may prevent them from being used interchangeably with Studio projects for purposes of your source control extension and change control processes.
    • Because of this difference, it would probably not be a good idea to populate ^||%Studio.Project with the name of the project in Atelier.
  • One option might be updating your source control extension (perhaps adding new menu items and UserAction/AfterUserAction handling for them) to support creating a %Studio.Project and associating/dissociating files with one, even from Atelier. This would support mixed use of Studio and Atelier and make migration simpler.
  • If you want to be able to detect in your source control extension code whether it is being called from Studio or Atelier, you can look at the StudioVersion property (defined in %Studio.Extension.Base). Particularly, this will contain the string "Atelier" if invoked from Atelier.

A nice enhancement for Atelier might be supporting a project definition that is shared between the client and server.

Timothy Leavitt · Sep 20, 2017 go to post

The current Atelier project is not exposed. What is your intended use case for it?

Regarding $Username - I suspect that the /api/atelier web application on your instance has only "Unauthenticated" selected under "Allowed Authentication Methods." If you change this to "Password" only and restart Atelier you should see $Username populated correctly.

Timothy Leavitt · Sep 14, 2017 go to post

If you're really interested in efficiency, one option might be put the list into a local array with $ListNext and then iterate over the array in reverse (either with a decrementing counter in a for loop, or using $order(array(sub),-1)). For example, rather than:

For i=$ListLength(tInputList):-1:1 {
	Set tValue = $ListGet(tInputList,i)
	// Do something with tValue
}

You could use something like the following (which accounts for null values in the $ListBuild list by skipping that index in the integer-subscripted array):

Kill tArray
Set tPointer = 0
While $ListNext(tInputList,tPointer,tValue) {
	If $Data(tValue) {
		Set tArray($i(tArray)) = tValue
	} Else {
		Set tArray = tArray + 1
	}
}

Set tKey = ""
For i=$Get(tArray):-1:1 {
	If $Data(tArray(i),tValue) {
		// Do something with tValue
	}
}

For "large enough" lists (the threshold for this depends on the length of the list elements), the second approach will be faster. (In testing, this performs better than putting the reversal of the original $ListBuild list into another $ListBuild list.)

More generally speaking: if you have a list that you only append to the end of (rather than needing to insert/shift elements randomly in the middle of it), and will need to iterate over it in reverse, a local array with integer subscripts is a better choice of data structure than a $ListBuild list.