A: Host and Target

Scott Waldron Updated by Scott Waldron

Storing the User's Previous Entries

There are times we would like to persist the user's last entries across invocations of the same command. Harmony extends Dino to allow setting user data that persists for the user's web session. This is done using the host object (more about that later). The two host methods we will be discussing are host.setUserData() and host.getUserData().

As mentioned in Aside: Script-Level Variable Lifetime, variables declared at the script-level only persist until the script is changed and saved. At that point they will be reset. User data does not have this restriction – it can survive a script change because the data is stored in a cache mechanism that is independent of the script.

On the Editor tab, you may have noticed a help panel to the left that contains various declarations about the currently targeted Type Key (or Applies To) if any, and at the bottom a long list of host.xxx entries.

Two of the more useful of these are host.setUserData() and host.getUserData(). They do exactly what their name says: get or set a piece of data that's private to the current user.

Enter the following into the Dino Cookbook editor:

private def _getFromDate() {
from System import DateTime;

var now = DateTime.Today;
return host.getUserData('from', DateTime(now.Year, 1, 1));
}

private def _getThruDate() {
from System import DateTime;

var now = DateTime.Today;
return host.getUserData('thru', DateTime(now.Year + 1, 1, 1));
}

/**
Command function demonstrating userData storage.
*/
def cmdStoreUserValues(dateFrom, dateThru) {
$$
<param name="dateFrom"
value="_getFromDate()" />
<param name="dateThru"
value="_getThruDate()" />
$$
host.setUserData('from', dateFrom);
host.setUserData('thru', dateThru);

return string.Format('From {0:yyyy-MM-dd}, Thru {1:yyyy-MM-dd}',
dateFrom, dateThru);
}

The host.getUserData() method takes two arguments: the first is the key that will be used to look up a specific piece of user data. This key is script scoped. What that means is that you can use the same key in a different script, and the two keys will not collide – they will hold distinct values. The second argument is the default value for this piece of user data if it has not yet been set.

As expected, when we run this command (remember to add a command record in the Commands tab), we initially get the default values (the beginning of the current year, and the beginning of the next year).

If we change these values and run the command, and then press the Run Again button:

As a general rule, host.getUserData() returns a value from a function that is called from a parameter declaration element, and host.setUserData() is called from the command function to set the user data to the argument that the user supplied when they executed the command. Following this pattern, you can ensure that user data is persisted across command invocations. And once your users get used to this, they will complain loudly if it doesn't behave this way. The example shown above is a very common pattern, almost to the point of being boilerplate.

Repeating a Command to Exhaust Results

Sometimes you need to collect information in steps. For example, you may need to get some information from the user, and then use that information to determine the data that will populate another data collection screen. Harmony has a special host method called host.nextCommand() that allows you to easily call another command record by simply providing its Id value.

Since a user may always cancel out of a command, it's acceptable to use host.nextCommand() to return the same command that is currently running. Since each iteration of the command invocation requires user interaction, there is no possibility here of creating an "endless loop".

In this next set of code, we'll create some fake "records" that the user can edit. We want the behavior of this to be "if the record has an empty Notes field, allow them to select it to edit the notes." Notice how, when you run the cmdAddMissingNotes function, if you add notes to a record, the next time the dialog comes up, that record is now missing.

private def _getFakeRecords() {
from Trestle import DynamicProxy;

var fakes = host.getUserData('fakes');
if not fakes {
//
// Create some fake "records" using dynamics
//
fakes = list[DynamicProxy](
dynamic(
Id: '1',
Name: 'Nerf Ball',
Price: 10.0,
Notes: ''
),
dynamic(
Id: '2',
Name: 'Slinky',
Price: 8.0,
Notes: ''
),
dynamic(
Id: '3',
Name: 'Barbie',
Price: 15.5,
Notes: ''
),
dynamic(
Id: '4',
Name: 'Matchbox',
Price: 5.35,
Notes: ''
),
);
host.setUserData('fakes', fakes);
}

return fakes;
}

private def _getFakeRecordSelections() {
from Trestle import DynamicProxy;

var selections = list[DynamicProxy]();
each r in _getFakeRecords() {
//
// Only add it to the selection list if the notes are empty
//
if not r.Notes {
var d = dynamic(
Name: r.Name,
Value: r.Id
);
selections.Add(d);
}
}

return selections;
}

/**
Command function demonstrating adding missing fields.
*/
def cmdAddMissingNotes(strFakeRecord, txtNotes, boolResetData) {
$$
<param name="strFakeRecord"
selections="_getFakeRecordSelections()" />
<param name="txtNotes" />
<param name="boolResetData"
help="Check this box to reset all notes." />
$$
each f in _getFakeRecords() {
if boolResetData {
f.Notes = '';
}
else if f.Id == strFakeRecord {
f.Notes = txtNotes;
break;
}
}

// show the command again to let the user keep going
return host.nextCommand('2219');
}

Editing a Set of Records with a Multi-Step Wizard

We can create a custom record editor quite easily by presenting the user with a set of records, allowing them to choose one, and then showing a form that presents the data that we want them to edit for that record. These command functions use the same _getFakeRecords and _getFakeRecordSelections functions that we used above, so we will not repeat that code here.

private def _getFakeName() {
return host.getUserData('_fake')?.Name;
}

private def _getFakePrice() {
return host.getUserData('_fake')?.Price;
}

private def _getFakeNotes() {
return host.getUserData('_fake')?.Notes;
}

/**
Command function that demonstrates the first step of a multi-step command.
*/
def cmdSelectFakeToEdit(strFakeRecord) {
$$
<param name="strFakeRecord"
selections="_getFakeRecordSelections()"
help="Select a fake product to edit." />
$$
each f in _getFakeRecords() {
if f.Id == strFakeRecord {
host.setUserData('_fake', f);
break;
}
}

// show the dialog for editing the properties of the selected record
return host.nextCommand('2221');
}

/**
Command function for subsequent step of a multi-step command.
*/
def cmdEditFakeRecord(strName, numPrice, txtNotes) {
$$
<param name="strName"
value="_getFakeName()" />
<param name="numPrice"
value="_getFakePrice()" />
<param name="txtNotes"
value="_getFakeNotes()" />
$$
var fake = host.getUserData('_fake');
fake.Name = strName;
fake.Price = numPrice;
fake.Notes = txtNotes;

// go back to the previous command and let them choose another record
return host.nextCommand('2220');
}

One thing to be aware of here is that we don't want the cmdEditFakeRecord to show up in our menu, because it should only be initiated by the previous command. So, for that command record, we set the Context to None (leave it blank) so that the user cannot select it directly.

The Command Target

Whenever a command is executed, the Harmony framework will supply a target object which can vary depending on the selected context. Up to this point, we have set the Context to List for all the commands we have created. What this means is that our target object will be a list of records that best represent the current state of the view we are in. Here are the two command functions and command records we'll use to show how Context mixes in with the _target() function.

/**
Command function showing a List target.
*/
def cmdShowListTarget() {
return _target();
}

/**
Command function showing a single record target.
*/
def cmdShowSingleTarget() {
var record = _target();

return string.Format('The {0} log was created on {1:yyyy-MM-dd}',
record.Name, record.DateCreated);
}

Once these are defined, navigate back to the System Log list screen. The Actions menu should show the Demonstrate List Target, but not the Demonstrate Single Record Target.

When we run the Demonstrate List Target command, we should get back a list of System Log records – basically what we are looking at on the screen.

Now open one of the System Log records for edit by clicking the pencil icon. The Actions menu in the resulting edit screen should have the Demonstrate Single Record Target command available:

Run the command. You should get back something like this:

You may have noticed that _target() is actually a function, not just a variable. This is because _target() is a special function known as a callback. Its value is not stored; rather it instructs Trestle to evaluate each time it is called and return the result.

Features and Limitations of _target()

In a List context, _target() will honor the current search term. In the System Log list page, enter a search term that will limit the list, like DateCreated > 2019-06-01.

Now run the Demonstrate List Target command again. You should see results that mirror what is currently shown in the view. Note that while the search term is honored by _target(), the paging mechanism is not considered.

Using _target() in a List context is ideal for periodic maintenance type commands, looping through searched records and modifying each one, for example:

def cmdDoSomethingToEach() {
var results = _target();
each res in results {
// do something here
SystemLog.GetRepository().Save(res);
}
}

In a List context, _target() will return all records that match the current search, up to a hard limit of 10,000 records.

More About the "host" Object

We'll be expanding the documentation in this section soon. Please contact us if you have questions.

How did we do?

A: Returning JSON Data

A: Command Arguments

Contact