Using Zen Components
Model View Controller
[Back] [Next]
   
Server:docs2
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

Model View Controller, or MVC, is a well known architecture for user interface design. This chapter describes how Zen implements MVC, and how to add MVC features to a Zen page.

To simplify the flow of data from a data source to a Zen page, Zen provides a set of classes that let you define a data model (the model) and connect it to a set of Zen components (the view) via an intermediate object (the controller). When the model associated with a controller changes, these changes are automatically broadcast to all views connected to the controller.
The following are some typical uses of MVC:
The following figure shows the three parts of the Zen MVC architecture — model, view, and controller — and indicates where these objects execute their code. The controller and its associated views are Zen components, placed on the Zen page. The controller component is hidden from view, but view components are user-visible. The view components display data values, which they obtain by requesting them from the controller. The controller resides on the client, but has the ability to execute code on the server. The model resides entirely on the server. It draws its data values from a source on the server and can respond to requests for data from the controller.
Model View Controller Architecture
The next three sections in this chapter expand the discussion of each part of the previous figure:
Remaining sections in this chapter provide a series of exercises that show how to use MVC features to create a form:
Model
A data model is any subclass of %ZEN.DataModel.DataModel. A data model can:
  1. Retrieve data values from one or more sources, such as:
  2. Place these values into its own properties.
  3. Make these properties available to be consumed by a data controller.
There are two variations on a data model class. Typically you choose one of these as your parent class when you create a new data model. The variations are closely related, but serve different purposes. The available data model subclasses are %ZEN.DataModel.ObjectDataModel and %ZEN.DataModel.Adaptor, as shown in the following figure.
Data Model Classes
%ZEN.DataModel.ObjectDataModel
A subclass of %ZEN.DataModel.ObjectDataModel is called an object data model. It defines one or more properties that a data controller component can consume. Each of these properties is correlated with a value from the data source class. Not every value in the data source needs to be exposed in the model. This convention allows you to expose in the data model only those values that you wish to.
An example of this might be a patient record, which contains confidential information that not every application should expose. Keep in mind that when you use this option, the developer of the object data model class is responsible for implementing methods to load values from a source into data model properties, store values back to a source, and validate values. This is in contrast to the adaptor data model, where Zen takes care of these details. However, this is not such a difficult procedure.
For more information about using %ZEN.DataModel.ObjectDataModel, consult the following sources:
%ZEN.DataModel.Adaptor
There are many times when it is convenient to use a persistent object as a data model. To make this easy to accomplish, Zen provides the %ZEN.DataModel.Adaptor interface. Adding this class as an additional superclass to a persistent class makes it possible to use the persistent class as a data model. This data model makes available to a data controller any and all properties that it contains. This option is useful when you want to expose every property in an existing class.
An example of this might be a class that you are using in an inventory or parts control application, wherein each product might be described by a class with a large number of properties. If you want to place all of these properties onto a form automatically without writing another class, you can simply cause the product class to extend %ZEN.DataModel.Adaptor. Following that, Zen simply generates the form for you, as later topics explain. The drawback of this choice is that your data and form are very closely linked. If you want some flexibility, you should subclass the object data model class and implement the internal interface. This is the classic trade-off of convenience versus flexibility.
For more information about using %ZEN.DataModel.Adaptor, consult the following sources:
Controller
A data controller manages the communication between a data model and a data view. A data controller is any subclass of %ZEN.Auxiliary.dataController. Through class inheritance, every data controller is also a Zen component, as the following figure shows. This convention permits you to place a data controller on a Zen page.
Data Controller and Data View Classes
<dataController>
To provide a data controller for a Zen page, simply add a <dataController> or a subclass of %ZEN.Auxiliary.dataController inside the <page>. The <dataController> component appears in XData Contents along with other components, but it is not visible onscreen. It acts as an intermediary between a data model and one or more data views.
The following example defines a <dataController> that opens an instance of the class MyApp.MyModel using an id value of 1. A <dynaForm> is bound to the <dataController> by setting the <dynaForm> controllerId property to the id of the <dataController>. This causes the <dynaForm> to display a form that provides a Zen control for every property within the modelClass.
<dataController id="data" modelClass="MyApp.MyModel" modelId="1"/>
<dynaForm id="myForm" controllerId="data"/>
<dataController> Attributes
When you place a <dataController> within a Zen <page>, you can assign it the following attributes:
Attribute Description
Zen component attributes
A <dataController> has the same general-purpose attributes as any Zen component. For descriptions, see these sections:
The id attribute is required for <dataController>. name and condition may also apply. A <dataController> is not visible, so visual style attributes do not apply.
alertOnError
If true, the <dataController> displays an alert box when it encounters errors while invoking server-side functions, such as when saving or deleting. The default is true.
alertOnError has the underlying data type %ZEN.Datatype.boolean. See Zen Attribute Data Types.”
autoRefresh Setting autoRefresh to a non-zero value turns on automatic refresh mode for this data controller. In this mode, the data controller reloads its data from the server at the periodic interval specified by autoRefresh (in milliseconds). autoRefresh is provided as a convenience for data controller used to drive meters or charts; it is of limited use for forms. Setting autoRefresh to 0 disables automatic refresh mode.
defaultSeries Optional. If a data model has multiple data series, defaultSeries is a 1-based number that specifies which series should be used to provide values to data views that can only display values from one data series (such as a form). The default is 1.
modelClass
Package and class name of the data model class that provides data for this <dataController>. The modelClass value can be a literal string, or it can contain a Zen #()# runtime expression.
modelId
String that identifies a specific instance of a data model object. The form and possible values of the modelId string are determined by the developer of the data model class. The modelId value can be a literal string, or it can contain a Zen #()# runtime expression.
oncreate
The oncreate event handler for the <dataController>. Zen invokes this handler each time the createNewObject method is called. See Zen Component Event Handlers.”
ondelete Client-side JavaScript expression that runs each time the deleteId method is called. The ondelete callback is invoked whether or not the delete succeeds. The ondelete callback can make use of two special variables: id contains the modelId of the deleted object, and deleted indicates whether or not the delete was successful.
onerror
Client-side JavaScript expression that runs each time the data controller attempts to open an instance of a data model object and encounters an error.
When you work with a %ZEN.Auxiliary.dataController programmatically, you can obtain the most recent error message reported by the data model object associated with this data controller, by examining the modelError property of the dataController object. This property is not available as an XML attribute when adding the <dataController> to the Zen page. To access this property from the client side, use the data controller’s client-side JavaScript method getError.
onnotifyController Client-side JavaScript expression that runs each time a data view connected to this data controller raises an event.
onsave Client-side JavaScript expression that runs each time the save method is called. The parameter id is passed to the event handler and contains the current modelId.
readOnly
If true, this data controller is read-only, regardless of whether or not its corresponding data model is read-only. The default is false.
readOnly has the underlying data type %ZEN.Datatype.boolean. See Zen Attribute Data Types.”
<dataController> Methods
A <dataController> or a subclass of %ZEN.Auxiliary.dataController provides the following client side JavaScript methods for working with the data controller and the data model that it represents. There are more methods described in the online Class Reference documentation for the %ZEN.Auxiliary.dataController class.
Client Side Method Purpose
getDataAsArrays() Returns the data in this controller as an array of arrays. This is useful when working with charts.
getDataAsObject(series)
Returns the data in this controller as an instance of a zenProxy object with properties whose names and values correspond to the properties of the current Data Model object.
If the data model supports more than one data series, then series (0-based) specifies which series to use (the default is 0).
For information about zenProxy, see Zen Proxy Objects in the “Zen Pages” chapter of Developing Zen Applications.
getDataByName(prop) Returns the data in the specified prop (property) of this data controller object. This data is equivalent to the value of the corresponding control on the generated form.
getDimensions()
Return number of dimensions within the dataModel. There are 2 dimensions: The first is the set of properties, the second has a typical size of 1.
The second dimension may be larger than 1 in cases where the model serves multiple series for a given model instance. (Such as when providing multiple data series for charts).
getDimSize(dim) Return the number of items in the specified dimension dim. dim is 1,2, or 3.
getLabel(n,dim) Get the label at position n in the given dimension dim. n is a 0–based number. dim is 1,2, or 3.
getModelClass() Return the current modelClass value.
getModelId() Return the current modelId value.
raiseDataChange() Notify listeners that the data associated with this data controller has changed.
setDataByName(p,v,s)
Change the specified property p of this data controller object to the value v. This also changes the value of the corresponding control on the generated form.
If p is "%id" change the id of this controller. If p is "%series" change the defaultSeries of this controller. If the data model supports more than one data series, then s (0-based) specifies which series to use (the default is 0).
Following a call (or multiple calls) to setDataByName, you must subsequently call raiseDataChange to notify listeners that the data associated with this data controller has changed.
setModelId(id) Change the modelId value at runtime. Changing the modelId value causes the controller to load a new record, and to update its associated views.
setModelClass(name) Change the modelClass value at runtime. Optionally, you can call setModelClass(name,id) to change both the modelClass and the modelId. Changing the modelClass value causes the controller to abandon the previous model and load data from the new model into the controller.
View
A data view is any Zen component that implements the %ZEN.Component.dataView interface. The list of data view components includes:
For an illustration, see the figure Data Controller and Data View Classes in the “Data Controller” section.
A data view component connects to its associated data controller at runtime, and uses it to get and set values from the associated data model. A data view points to its data controller; more than one data view can point to the same data controller.
Data View Attributes
Data view components support the usual Zen component attributes, plus any specialized attributes that are typical of the specific type of component. Additionally, all data view components support the following attributes, which relate specifically to the component’s role as a data view.
Important:
If a user sets a component’s value by interacting with the Zen page, the controller and thus the model are notified. If program code sets the value, the controller is not notified, and the value is lost on submit. Program code should write directly to the controller, which then updates the control.
Attribute Description
controllerId Identifies the data controller for this data view (). The controllerId value must match the id value provided for that <dataController> component.
onnotifyView
The onnotifyView event handler for the data view component. Zen invokes this handler each time the data controller associated with this data view raises an event. See Zen Component Event Handlers.”
Meters and controls support the following property:
Attribute Description
dataBinding
Identifies the data model property that is bound to this component. This property provides the value that the component displays:
  • If the dataBinding value is a simple property name, this is assumed to be a property within the data model class identified by the <dataController> modelClass attribute.
  • Alternatively, dataBinding can provide a full package, class, and property name.
dataBinding is generally suitable for components that display a single value (meters or controls). Each meter on a Zen page must supply a controllerId and a dataBinding. Controls do not support the data view interface, so cannot supply a controllerId, but if the form that contains the controls has an associated data controller, each control within the form can supply a dataBinding attribute that identifies which property it displays.
The Controller Object
When you work with a data view component programmatically, you can obtain a reference to the associated %ZEN.Auxiliary.dataController object via the following properties of the %ZEN.Component.dataView interface:
Multiple Data Views
Whenever a user modifies a value within one of the controls that is bound to a data controller, the data controller is notified. It is common for data views to share a controller; for example, different types of chart on the same page could share the same data controller to display different visualizations of the same data, as in the following figure. If there are multiple data view components connected to the same data controller, they are all notified of any change to a bound control.
For examples of shared data controllers, use Studio to view the classes ZENMVC.MVCChart and ZENMVC.MVCMeters. The class ZENMVC.MVCMeters uses the same data controller to provide values for a <dynaGrid> and several meters. The class ZENMVC.MVCChart provides a <dynaGrid> and three charts that all use the same data controller. Try entering the following URIs in the browser:
http://localhost:57772/csp/samples/ZENMVC.MVCChart.cls
http://localhost:57772/csp/samples/ZENMVC.MVCMeters.cls
Where 57772 is the web server port number that you have assigned to Caché.
When you change a value in one of these pages by editing it in the <dynaGrid> and pressing Enter, this change affects the corresponding value in all the charts (or meters) on the same page, because all of them share the same data controller. In the following figure, the user has just modified the Trucks field in the <dynaGrid> for ZENMVC.MVCChart.
Constructing a Model
This topic provides the first in a series of exercises that show how to use the Model View Controller to create a form. If you have a new Caché installation, before you begin these exercises you must first run the ZENDemo home page. Loading this page silently generates data records for the SAMPLES namespace. You only need to do this once per Caché installation.
Enter the following URI in the browser:
http://localhost:57772/csp/samples/ZENDemo.Home.cls
Where 57772 is the web server port number that you have assigned to Caché.
Step 1: Type of Model
There are two basic choices when generating a form using the Model View Controller:
For this exercise we choose an object data model and a <form>.
Step 2: Object Data Model
Suppose a persistent object called Patient contains a patient record. This object may have hundreds of properties. Suppose you want to create a simple page that only displays demographic information, in this case the patient’s name and city of residence. This is a clear case for using %ZEN.DataModel.ObjectDataModel.
First, define an object data model class called PatientModel that knows how to load and store properties from the Patient object:
  1. Start Caché Studio.
  2. Choose the SAMPLES namespace.
  3. Choose File > New or Ctrl-N or the icon.
  4. Click the General tab.
  5. Click the Caché Class Definition icon.
  6. Click OK.
  7. For Package Name enter:
  8. In the Class Name field, type:
  9. Click Next.
  10. Choose Extends
  11. Click Next and enter (or browse to) this class name:
  12. Click Finish.
    Studio creates and displays a skeletal object data model class:
    Class MyApp.PatientModel Extends %ZEN.DataModel.ObjectDataModel
    {
    
    }
  13. Add properties and methods to complete the class as shown in the following code example. This example defines two properties for the data model (Name and City). It also overrides several of the server-side methods in the %ZEN.DataModel.ObjectDataModel interface. For documentation of these methods, see the section Object Data Model Callback Methods.”
    Class MyApp.PatientModel Extends %ZEN.DataModel.ObjectDataModel
    {
      Property Name As %String;
      Property City As %String;
    
      /// Load an instance of a new (unsaved) source object for this DataModel.
      Method %OnNewSource(Output pSC As %Status = {$$$OK}) As %RegisteredObject
      {
        Quit ##class(ZENDemo.Data.Patient).%New()
      }
    
      /// Save instance of associated source object.
      Method %OnSaveSource(pSource As ZENDemo.Data.Patient) As %Status
      {
        Set tSC=pSource.%Save()
        If $$$ISOK(tSC) Set ..%id=pSource.%Id()
        Quit tSC
      }
    
      /// Load an instance of the source object for this DataModel.
      Method %OnOpenSource(pID As %String, pConcurrency As %Integer = -1,
                           Output pSC As %Status = {$$$OK}) As %RegisteredObject
      {
        Quit ##class(ZENDemo.Data.Patient).%OpenId(pID,pConcurrency,.pSC)
      }
    
      /// Delete instance of associated source object.
      ClassMethod %OnDeleteSource(pID As %String) As %Status
      {
       Quit ##class(ZENDemo.Data.Patient).%DeleteId(pID)
      }
    
    
      /// Do the actual work of loading values from the source object.
      Method %OnLoadModel(pSource As ZENDemo.Data.Patient) As %Status
      {
        Set ..Name = pSource.Name
        Set ..City = pSource.Home.City
        Quit $$$OK
      }
    
      /// Do the actual work of storing values into the source object.
      Method %OnStoreModel(pSource As ZENDemo.Data.Patient) As %Status
      {
        Set pSource.Name = ..Name
        Set pSource.Home.City = ..City
        Quit $$$OK
      }
    }
  14. Choose Build > Compile or Ctrl-F7 or the icon.
Binding a <form> to an Object Data Model
This exercise creates a data controller based on the object data model from the previous exercise, Constructing a Model.” It then binds a form to this data controller.
Step 1: Data Controller
If you do not already have a simple Zen application and page class available from previous exercises, create them now using instructions from the “Zen Tutorial” chapter in Using Zen:
Now place a data controller component on the Zen page by adding a <dataController> element in the MyApp.MyNewPage class XData Contents block, inside the <page> container, as follows:
<dataController id="patientData"
                modelClass="MyApp.PatientModel"
                modelId="1" />
Where:
Note:
The <dataController> component is not visible on the page.
Step 2: Data View
Now create a form and connect it to the data controller, as follows:
  1. Place a <form> component inside the <page> container.
    Bind the form to the data controller that you created in Step 1: Data Controller by setting the <form> controllerId to match the <dataController> id value. For example:
    <form controllerId="patientData" id="MyForm" >
    </form>
    The id attribute does not affect the binding but becomes useful in a future step, when we save the form.
  2. Within the <form> add two <text> controls.
    Bind each control to a property of the data model by providing a dataBinding attribute that identifies a property within the modelClass of the <dataController>. Also provide a label for each control. For example:
    <form controllerId="patientData" id="MyForm" >
      <text label="Patient Name" dataBinding="Name" />
      <text label="Patient City" dataBinding="City" />
    </form>
    The label attribute does not have any purpose relative to the Model View Controller, but it is necessary if we want our controls to have a meaningful labels on the Zen page.
Step 3: Initial Results
View your initial results as follows:
  1. Open your Zen page class in Studio.
  2. Choose Build > Compile or Ctrl-F7 or the icon.
  3. Choose View > Web Page or the icon.
    The data controller component creates a MyApp.PatientModel object on the server, and asks it to load data from data source record number 1 (identified by the <dataController> modelId attribute) into its own properties. The data controller places these data values into the appropriate controls within the form. The dataBinding attribute for each control identifies which property provides the value for that control, Name or City.
    The following figure shows our form with the current values from record 1.
Step 4: Saving the Form
Suppose you want the user to be able to edit the values in this form, and to save changes. To save the values in a form associated with a data controller, your application must call the form’s save method. Typically you would enable this as follows:
  1. Add a client-side method to the page class as follows:
    ClientMethod save() [ Language = javascript ]
    {
      var form = zen('MyForm');
      form.save();
    }
    
    Now you can see why it was important to define an id for our <form>. The JavaScript function zen needs this id to get a pointer to the form so that we can call its save method.
  2. Add to your page a <button> that calls this method to save the form.
    The entire <page> definition now looks something like this:
    <page xmlns="http://www.intersystems.com/zen" title="">
      <dataController id="patientData"
                      modelClass="MyApp.PatientModel"
                      modelId="1" />
      <form controllerId="patientData" id="MyForm">
        <text label="Patient Name" dataBinding="Name" />
        <text label="Patient City" dataBinding="City" />
      </form>
      <button caption="Save" onclick="zenPage.save();"/>
    </page>
  3. Try editing the data and clicking Save.
Each time the user clicks the Save button, the form save method calls back to the server and saves the data by calling the appropriate methods of the data model class. During this process, the form asks the data controller to assist with data validation of the various properties. The source of this validation logic is the data model class.
The data controller also has a save method we can use. There is a difference between saving the form and saving the controller. Calling the save method of the form triggers the form validation logic, after which the form instructs the controller to save data by calling the appropriate methods of the data model class. Saving the controller skips form validation.
You can try out basic form validation in step 3 of this example as follows: If you empty the Patient Name field entirely and click Save, a validation error occurs. This is because the Patient property is marked as Required in the ZENDemo.Data.Patient class that serves as our data source. However, if you change the Patient Name or Patient City to any non-empty value, the form saves correctly. It is easier to prove this to yourself once you have extended the form to allow you to easily view more than one data record. Then you can switch back and forth between records to see that they in fact contain your changes.
Note:
For further validation examples, try using and viewing the Zen page class ZENMVC.MVCForm in the SAMPLES namespace. Try entering the following URI in the browser:
http://localhost:57772/csp/samples/ZENMVC.MVCForm.cls
Where 57772 is the web server port number that you have assigned to Caché. Edit values in one of the forms shown on the page, and click a Submit button. You can use Studio to view the class code.
Step 5: Performing Client-side Validation
Rather than wait for server-side validation, we can add client-side validation to the data model class by defining a property-specific IsValidJS method. This is a JavaScript ClientClassMethod that uses the naming convention propertyIsValidJS and returns an error message if the value of the given property is invalid or '' (an empty string) if the value is OK. This method can be defined within the data model class, or you can define a datatype class that defines an IsValidJS method.
Adding the following method to the MyApp.PatientModel class causes the data controller to automatically apply this validation to the City property on the client each time its save method is called:
ClientClassMethod CityIsValidJS(value) [Language = javascript]
{
  return ('Boston' == value) ? 'Invalid City Name' : '';
}
Step 6: Setting Values Programmatically
In order to set data values programmatically, set the value in the controller and then tell the controller to notify all of its views of the change. To set the value, you can use the controller method setDataByName, and then use raiseDataChange to notify the views. You have to call raiseDataChange explicitly, which allows you to change multiple values in the controller and only raise the event once.
ClientMethod ChangeValue() [ Language = javascript ] {
  var controller = zenPage.getComponentById('patientData');
  controller.setDataByName('Name','Public,John Q');
  controller.raiseDataChange();
}
Adding Behavior to the <form>
The %ZEN.Auxiliary.dataController class offers several useful methods. Suppose you want to enhance your page from the previous exercise, Binding a <form> to an Object Data Model,” so that it can open new records, create new records, delete existent records, or reset the current record to a particular model ID. This topic explain how to accomplish these tasks using dataController methods such as getModelId and setModelId.
Step 1: Opening a New Record
When you ask the browser to display the page you have been building during these exercises, it always displays the same data record. This is because you have configured the modelId property of your <dataController> element with the value of 1. You can see what happens if you change this property to other values, such as 2, 3, or 4, up to 1000.
However, you must remember that these values represent real ID values of existing instances of the ZENDemo.Data.Patient class. These instances exist because all of the classes in ZENDemo.Data are populated automatically when you first run The Zen Demo as described in the “Introducing Zen” chapter of Using Zen.
Suppose you want to give the user the option of choosing which record to view. There are several options, including:
It does not matter which option you choose for user input. The key task is for your page to be able to tell the data controller to load the record. You can accomplish this as follows:
  1. Open your Zen page class in Studio.
  2. Add a new text field to the <form>:
    <form controllerId="patientData" id="MyForm">
      <text label="ID:"
            onblur="zenPage.loadRecord(zenThis.getValue())"
            dataBinding="%id"/>
      <text label="Patient Name" dataBinding="Name" />
      <text label="Patient City" dataBinding="City" />
    </form>
    
  3. Add a corresponding client-side method:
    ClientMethod loadRecord(id) [ Language = javascript ]
    {
      var controller = zen('patientData');
      controller.setModelId(id);
    }
    
    The onblur event calls a client-side method loadRecord that first gets a pointer to the data controller using its id value "patientData", then uses whatever the user has entered in the <text> field as a modelId to load the desired record from the data model. To actually load the record, loadRecord uses the data controller method setModelId.
    Also observe that this example binds the ID field to the %id property of the data model, so that this field always shows you the ID of the current record. This step is not necessary for setModelId to work, but it is very useful in Step 2: Creating and Deleting Records in this exercise.
  4. Choose Build > Compile or Ctrl-F7 or the icon.
  5. Choose View > Web Page or the icon.
    The following figure shows the form.
  6. Try the onblur functionality as follows:
  7. In the exercise Binding a <form> to an Object Data Model,” during Step 4: Saving the Form,” you added Save functionality without the ability to easily test it. Try it now, as follows:
Step 2: Creating and Deleting Records
Creating and deleting records are also simple tasks. Modify your page to add the necessary buttons and client side code as follows.
  1. Open your Zen page class in Studio.
  2. After the <form> and before the closing </page>, replace the single Save button with the following <hgroup>:
    <hgroup>
      <button caption="Save" onclick="zenPage.save()"/>
      <button caption="New" onclick="zenPage.newRecord()"/>
      <button caption="Update" onclick="zenPage.updateRecord()"/>
      <button caption="Delete" onclick="zenPage.deleteRecord()"/>
    </hgroup>
    These statements add the buttons to an <hgroup> so that they appear on the screen in a row. Each button defines its onclick method as a different client-side method.
  3. Add the corresponding new client-side methods to the page class:
    ClientMethod newRecord() [ Language = javascript ]
    {
      var controller = zen('patientData');
      controller.createNewObject();
    }
    
    And:
    ClientMethod updateRecord() [ Language = javascript ]
    {
      var controller = zen('patientData');
      controller.update();
    }
    And:
    ClientMethod deleteRecord() [ Language = javascript ]
    {
      var controller = zen('patientData');
      controller.deleteId(controller.getModelId());
      controller.createNewObject();
    }
    Each of these methods uses a different dataController method to achieve its purposes.
  4. Choose Build > Compile or Ctrl-F7 or the icon.
  5. Choose View > Web Page or the icon.
    The following figure shows the form.
New
Suppose the user clicks New. Calling createNewObject immediately creates a new empty model. In the browser, every time the user clicks New the form is emptied. After that, if the user completes the empty form and clicks Save, this invokes the form’s save method. This (eventually) leads to a call to the data model’s %OnSaveSource method on the server.
In the exercise Binding a <form> to an Object Data Model,” during Step 4: Saving the Form,” you added Save functionality by causing the %OnSaveSource method to set the %id property of the model to the ID of the saved object, as follows:
Method %OnSaveSource(pSource As ZENDemo.Data.Patient) As %Status
  {
    Set tSC=pSource.%Save()
    If $$$ISOK(tSC) Set ..%id=pSource.%Id()
    Quit tSC
  }
As a result, every time the user clicks New, enters values, then clicks Save, the form shows the newly assigned ID to the user (thanks to the dataBinding on that field). Of course, this only works if the data entered in the form passes validation. Name is a required field, so if no Name is entered, the record is not saved.
Delete
Suppose the user clicks Delete. The data controller’s deleteId method expects to receive an input argument containing the ID for the record to be deleted. Therefore, when the user clicks Delete, the page uses the data controller’s getModelId method to determine the ID of the record the user is currently viewing. It passes this ID on to deleteId. This (eventually) leads to a call to the data model’s %OnDeleteSource method on the server. The source object is deleted, and since there is no longer source object, the page calls the data controller’s createNewObject method to empty the form and prepare it for new input.
Although the code examples in this chapter do not take advantage of this feature, the deleteId method returns a Boolean value, true or false. It is true if it successfully deleted the record. It is false if it failed, or if the data controller or its data model are read-only. A data controller is read-only if its readOnly attribute is set to 1 (true). A data model is read-only if its class parameter is set to 1 (true).
Important:
If, while using this exercise, you delete a record with a specific ID, this object no longer exists. You cannot view or create a record with this ID again.
Update
Before clicking Update, the user must enter an ID number (between 1 and 1000) in the ID field. The page updates the form fields with data from that record. This fails only if you have previously deleted a record with that ID.
Errors
A data controller has a server-side property called modelError that is a string containing the most recent error message that the data controller encountered while saving, loading, deleting, or invoking a server-side action. A data controller also has a client-side JavaScript method, getError, that an application can invoke to get the modelError value. getError has no arguments and returns the modelError string. It returns an empty string '' if there is no current error.
<dynaForm> with an Object Data Model
This topic explains how to use <dynaForm> with a data controller. In this case the data model is an object data model.
Step 1: <dynaForm> is Easy
You may create your first <dynaForm> very easily as follows:
  1. Create a new Zen page class. Use the instructions from the exercise Creating a Zen Page in the “Zen Tutorial” chapter of Using Zen. Call your new class anything you like, but keep it in the MyApp package. Be careful not to overwrite any of your previous work.
  2. In XData Contents, place a <dataController> and <dynaForm> inside <page>:
    <page xmlns="http://www.intersystems.com/zen" title="">
      <dataController id="patientData"
                      modelClass="MyApp.PatientModel"
                      modelId="1" />
      <dynaForm controllerId="patientData"/>
    </page>
  3. Choose Build > Compile or Ctrl-F7 or the icon.
  4. Choose View > Web Page or the icon.
    The form displays two fields that contain the current Name and City values for the record whose modelId you entered in the <dataController> statement. Perhaps you have changed these values, or deleted this record, during previous exercises. Whatever data is now available for that modelId displays.
    The label for each control is determined by the corresponding property name in MyApp.PatientModel. These labels are different from the text you assigned to the caption attribute when you used <form> and <text> components to lay out the form. In all other respects, this display is identical to the display you first saw in the exercise Binding a <form> to an Object Data Model,” during Step 3: Initial Results.” Later steps show how to set specific labels for the controls in a <dynaForm>.
Step 2: Converting to <dynaForm>
Now you are ready to recreate the <form> example from previous exercises in this chapter as a <dynaForm>. To do this, you need to rewrite your MyApp.MyNewPage XData Contents block so that it looks like this:
<page xmlns="http://www.intersystems.com/zen" title="">

  <dataController id="patientData"
                  modelClass="MyApp.PatientModel" />

  <dynaForm id="MyForm"
            controllerId="patientData"
            defaultGroupId="generatedFields">
    <text label="ID:"
          onblur="zenPage.loadRecord(zenThis.getValue())"
          dataBinding="%id"/>
    <vgroup id="generatedFields"/>
  </dynaForm>

  <hgroup>
    <button caption="Save" onclick="zenPage.save()"/>
    <button caption="New" onclick="zenPage.newRecord()"/>
    <button caption="Update" onclick="zenPage.updateRecord()"/>
    <button caption="Delete" onclick="zenPage.deleteRecord()"/>
  </hgroup>
</page>
That is:
  1. In Studio, return to your existing sample page, MyApp.MyNewPage.
  2. Remove or comment out this <form>, which specifies each control individually:
    <form id="MyForm"
          controllerId="patientData" >
      <text label="ID:"
            onblur="zenPage.loadRecord(zenThis.getValue())"
            dataBinding="%id"/>
      <text label="Patient Name" dataBinding="Name" />
      <text label="Patient City" dataBinding="City" />
    </form>
  3. Add this <dynaForm>, which relies on the data controller to supply it with any control definitions that can be generated based on properties in the data model:
    <dynaForm id="MyForm"
              controllerId="patientData"
              defaultGroupId="generatedFields">
      <text label="ID:"
            onblur="zenPage.loadRecord(zenThis.getValue())"
            dataBinding="%id"/>
      <vgroup id="generatedFields"/>
    </dynaForm>
    Where:
  4. Choose Build > Compile or Ctrl-F7 or the icon.
  5. Choose View > Web Page or the icon.
    The following <dynaForm> displays:
  6. You may assign specific labels to the generated controls on the <dynaForm> as follows:
  7. Refresh your Zen page MyApp.MyNewPage in the browser.
    Your <dynaForm> and <form> now produce identical results.
Step 3: Automatic Control Selection
When you use <dynaForm> instead of <form>, it is no longer necessary to add data view components (controls) to the form, item by item, as described in the section Binding a <form> to an Object Data Model.” <dynaForm> automatically extracts this information from the data model at compile time.
The following exercise demonstrates the use of a <dynaForm> by adapting your page class from previous exercises so that it uses a different data model. When you complete the exercise and display the page, a new form appears whose controls are clearly different from those in previous exercises:
  1. Return to Studio in the SAMPLES namespace.
  2. The Class Copy dialog displays. Enter:
    The new class definition for MyApp.EmployeeModel displays in Studio.
  3. Edit MyApp.EmployeeModel so that it has the following three properties only:
    Property Name As %String;
    Property Salary As %Numeric;
    Property Active As %Boolean;
    
  4. Edit the methods inside MyApp.EmployeeModel to work with the new properties:
    Method %OnLoadModel(pSource As ZENDemo.Data.Employee) As %Status
    {
      Set ..Name = pSource.Name
      Set ..Salary = pSource.Salary
      Set ..Active = pSource.Active
      Quit $$$OK
    }
    And:
    Method %OnStoreModel(pSource As ZENDemo.Data.Employee) As %Status
    {
      Set pSource.Name = ..Name
      Set pSource.Salary = ..Salary
      Set pSource.Active = ..Active
      Quit $$$OK
    }
  5. Choose Build > Compile or Ctrl-F7 or the icon.
  6. The Class Copy dialog displays. Enter:
    The new class definition for MyApp.MyOtherPage displays in Studio.
  7. Take a shortcut by leaving the <dataController> id as "patientData".
    You may ignore this shortcut by globally replacing "patientData" with a more meaningful id, for example "employeeData". However, make sure you change all the instances of this id string in the class to avoid errors at runtime.
  8. Change the <dataController> modelClass to "MyApp.EmployeeModel".
  9. Choose Build > Compile or Ctrl-F7 or the icon.
  10. Choose View > Web Page or the icon.
    The following figure shows the resulting form, with the values for record 5.
    <dynaForm> has chosen controls for this form as follows:
<dynaForm> Controls Based on Data Types
<dynaForm> determines which type of control to assign to each property in the model based on the data type of that property. The following table match property data types with the <dynaForm> controls they generate.
Note:
If you do not like the choices that <dynaForm> makes, you can switch to <form> and bind each control to a property individually, using the dataBinding attribute as described in the exercise Binding a <form> to an Object Data Model during Step 2: Data View.”
<dynaForm> Controls Based on Data Types
Data Type Details Control
%ArrayOfDataTypes See Array Data Types following the table. <textarea>
%Boolean <checkbox>
%Date In YYYY-MM-DD format <dateSelect>
%Date In other formats <text>
%Enumerated Using a VALUELIST with 4 or fewer values. <radioSet>
%Enumerated Using a VALUELIST with more than 4 values <combobox>
%ListOfDataTypes See List Data Types following the table. <textarea>
%Numeric <text>
Object reference <dynaForm> generates an SQL query <dataCombo>
Stream %CharacterStream <textarea>
Stream %BinaryStream <image>
%String With MAXLEN over 250 <textarea>
%String With MAXLEN between 1 and 250 <text>
Public properties All types not listed above <text>
Private properties Any properties marked private Not displayed
Most of the data types listed in the previous table are defined as Caché classes. As such, they can define class parameters, including the VALUELIST, DISPLAYLIST, and MAXLEN parameters mentioned in the table. These parameters provide details about the data type.
For %Enumerated properties, the VALUELIST parameter specifies the internal values (1, 2, 3) and the DISPLAYLIST parameter specifies the names that are displayed for the user to choose (High, Medium, Low). For %String properties, the MAXLEN parameter specifies a maximum length.
Many other class parameters are available. For details, see the Parameters section in the “Data Types” chapter of Using Caché Objects.
List Data Types
For a property whose data type is %ListOfDataTypes, Zen streams the list collection to the client as one string delimited by carriage return characters. The resulting <textarea> control displays one collection item per line of text.
For a <dynaForm>, this convention works when the data model class sets this property parameter:
ZENCONTROL="textarea"
For details, see Data Model Property Parameters in the “Data Model Classes” section of this chapter.
For a <form>, this convention works when you bind a <textarea> control to the property of type %ListOfDataTypes using the dataBinding attribute.
See the exercise Binding a <form> to an Object Data Model during Step 2: Data View.”
Array Data Types
For a property whose data type is %ArrayOfDataTypes, the conventions are the same as for %ListOfDataTypes, except that the serialized string takes this form:
key:value[CR]key:value[CR]key:value
Where : is a single colon and [CR] represents a single carriage return character.
<dynaForm> with an Adaptor Data Model
This topic explains how to use <dynaForm> with a data controller when the data model is an adaptor data model. This approach is particularly convenient when you have an existing class with a large number of properties that you need to display on a form. In that case it would be extremely time-consuming to add these properties one by one to a subclass of %ZEN.DataModel.ObjectDataModel, as demonstrated in the previous exercises in this chapter.
<dynaForm> can save coding time, especially when you use it in combination with a subclass of %ZEN.DataModel.Adaptor. All you need to do then is to create a Zen page class whose <page> contains a <dataController> and a <dynaForm>. Your subclass of %ZEN.DataModel.Adaptor becomes the model, the view, and the controller, all in one. All of its properties become controls on the resulting <dynaForm>. Zen generates the appropriate control type for property automatically.
Step 1: Generating the Form
The basic outline for using <dynaForm> with an adaptor data model is as follows:
Note:
This example is based on the Zen classes ZENMVC.MVCDynaForm, ZENMVC.Person, and ZENMVC.Address in the SAMPLES namespace.
  1. Edit a persistent class so that it also extends %ZEN.DataModel.Adaptor, for example:
    Class ZENMVC.Person Extends (%Persistent, %ZEN.DataModel.Adaptor)
    {
      Property Name As %String [ Required ];
      Property SSN As %String;
      Property DOB As %Date;
      Property Salary As %Numeric;
      Property Active As %Boolean;
      Property Home As Address;
      Property Business As Address;
    }
    
    The Person class includes properties defined by another class, Address, which must also extend %ZEN.DataModel.Adaptor but which need not be persistent, for example:
    Class ZENMVC.Address Extends (%SerialObject, %ZEN.DataModel.Adaptor)
    {
      Property City As %String(MAXLEN = 50);
      Property State As %String(MAXLEN = 2);
      Property Zip As %String(MAXLEN = 15);
    }
  2. Compile both data model classes.
  3. Create a new Zen page class.
  4. In XData Contents, place a <dataController> and <dynaForm> inside <page>:
    <page xmlns="http://www.intersystems.com/zen" title="">
      <dataController id="source" modelClass="ZENMVC.Person" modelId=""/>
      <dynaForm id="MyForm" controllerId="source" />
    </page>
  5. Compile the Zen page class.
  6. Choose View > Web Page or the icon.
    <dynaForm> generates the appropriate controls and displays the form, for example:
For a table that matches data types with the <dynaForm> controls they generate, see the <dynaForm> Controls Based on Data Types table in this chapter.
Step 2: Property Parameters
Once you have the basics in place, you may refine the form by assigning data model parameters to the properties in the persistent class that you are using as an adaptor data model. For example:
  1. Open the data model class Person from Step 1: Generating the Form in this exercise.
  2. Add data model parameters to the properties as shown below:
    Class ZENMVC.Person Extends (%Persistent, %ZEN.DataModel.Adaptor)
    {
      Property Name As %String (ZENLABEL = "Employee Name") [ Required ];
      Property SSN As %String (ZENREADONLY = 1);
      Property DOB As %Date (ZENLABEL = "DateofBirth", ZENATTRS="format:DMY");
      Property Salary As %Numeric (ZENHIDDEN = 1);
      Property Active As %Boolean
        (ZENLABEL = "Is this person working?", ZENATTRS="showLabel:false");
      Property Home As Address;
      Property Business As Address;
    }
    
  3. Compile the Person class.
  4. Refresh your view of the Zen page class in the browser.
    <dynaForm> generates the appropriate controls and displays the form, for example:
    The data model parameters have the following effects:
For a table that lists more data model parameters, and explains their effects on generated controls in the <dynaForm>, see the section Data Model Property Parameters.”
Step 3: Adding Behavior to the <dynaForm>
Adding behavior to a <dynaForm> is quite similar to the steps for <form>. For <dynaForm>, the steps are:
  1. Open the Zen page class from Step 2: Property Parameters.”
  2. Add a <text> control to display the current model ID value, and buttons to permit Update, New, and Save operations, as follows:
    <page xmlns="http://www.intersystems.com/zen" title="">
    
      <text label="Model ID:" id="idText" dataBinding="%id"
            onblur="zenPage.loadRecord(zenThis.getValue())" />
      <spacer height="10"/>
    
      <dataController id="source" modelClass="ZENMVC.Person" modelId=""/>
      <dynaForm id="MyForm" controllerId="source" />
    
      <hgroup>
        <button caption="Update" onclick="zenPage.showRecord();" />
        <button caption="New" onclick="zenPage.newRecord();" />
        <button caption="Save" onclick="zenPage.save();" />
      </hgroup>
    
    </page>
  3. Add the corresponding new client-side methods to the page class:
    ClientMethod loadRecord(id) [ Language = javascript ]
    {
      var controller = zen('source');
      controller.setModelId(id);
    }
    And:
    ClientMethod showRecord() [ Language = javascript ]
    {
      var controller = zen('source');
      controller.update();
    }
    And:
    ClientMethod newRecord() [ Language = javascript ]
    {
      var text = zen('idText');
      text.setValue("");
      var controller = zen('source');
      controller.createNewObject();
    }
    And:
    ClientMethod save() [ Language = javascript ]
    {
      var form = zen('MyForm');
      form.save();
    }
    
  4. Compile the Zen page class.
  5. Choose View > Web Page or the icon.
  6. Click the New button.
  7. Enter data in the fields (except the Model ID and the read-only SSN field) and click Save.
  8. Click the New button again, just to clear the fields.
  9. Enter the number 1 in the ID field and click Update. The corresponding record displays. For example:
    Since you have provided no special format for model ID values in the Person class, the default prevails. This means each new record you add gets a sequential number starting at 1. You may add more records by repeating steps 6 and 7. The numbers increment automatically.
    If you try to view a record by entering an ID number that does not exist, the <dynaForm> displays with all of its fields disabled. You may click New to redisplay an active form in which to enter data for a new record.
Step 4: Virtual Properties
If you want to interject changes in a data model before using it, you can override the %OnGetPropertyInfo method in the data model class. This is the way to add virtual properties that you want to use in the model, but that do not exist in the class that you began with. In order to use virtual properties, you must ensure that the data model class parameter DYNAMICPROPERTIES is set to 1 (true). Its default value in the %ZEN.DataModel.Adaptor class is 0 (false). You can use %OnGetPropertyInfo with either type of data model, but it makes the most sense for an adaptor data model because in that case you are using an existing class as a data model.
This exercise adds a %OnGetPropertyInfo method to the adaptor data model class Person from the previous exercises in this chapter:
  1. Open the Person class in Studio.
  2. Select %OnGetPropertyInfo and click OK.
    Studio adds a skeleton %OnGetPropertyInfo method to the class.
  3. Edit the method as follows:
    These statements add a <checkbox> and a <textarea> to any <dynaForm> generated by the model.
    ClassMethod %OnGetPropertyInfo(pIndex As %Integer,
                                   ByRef pInfo As %String,
                                   pExtended As %Boolean = 0) As %Status
    {
      #; Increment past the 3 embedded properties from the last Address object.
      #; This is not necessary when the last property in the Person object
      #; is a simple data type such as %String or %Boolean or %Numeric.
      Set pIndex = pIndex + 3
    
      #; add a field at the end of the form
      Set pInfo("Extra") = pIndex
      Set pInfo("Extra","%type") = "checkbox"
      Set pInfo("Extra","caption") = "Extra!"
      Set pInfo("Extra","label") = "This is an extra checkbox."
      Set pIndex = pIndex + 1
    
      #; add another field at the end of the form
      Set pInfo("Comments") = pIndex
      Set pInfo("Comments","%type") = "textarea"
      Set pInfo("Comments","caption") = "Please enter additional comments:"
       Set pIndex = pIndex + 1
    
      Quit $$$OK
    }
    
  4. Recompile the Person class.
  5. Refresh your view of the Zen page class in the browser.
    <dynaForm> generates the appropriate controls and displays the form, for example:
Data Model Classes
The basic behavior of data models comes from the abstract base class %ZEN.DataModel.DataModel. This class defines the basic data model interface which is, in turn, implemented by subclasses. A data model class can be one of the following types:
Data Model Class Properties
The %ZEN.DataModel.DataModel class provides the following properties:
Data Model Class Parameters
Data model classes provide class parameters that determine the type of data model. The following table lists them.
Data Model Class Parameters
Property Parameter Description
DOMAIN Available for subclasses of %ZEN.DataModel.ObjectDataModel only. You must provide a value for this parameter if you wish to use Zen localization.
DYNAMICPROPERTIES
1 (true) or 0 (false). If true, this model supports virtual properties. For background information, see the section Virtual Properties.” The default value for DYNAMICPROPERTIES is:
READONLYMODEL 1 (true) or 0 (false). If true, indicates that this is a read-only model. It can be used to display data but not to generate editable forms. The default is 0 (false).
Data Model Property Parameters
The %ZEN.DataModel.ObjectDataModel class provides property parameters that you can apply to the data model properties that you wish to use as controls on a form. These parameters let you provide more specific control over the properties of the data model class. The utility class %ZEN.DataModel.objectModelParameters defines these parameters; the following table lists them.
Note:
For examples, use Studio to view the classes ZENMVC.FormDataModel and ZENMVC.FormDataModel2 in the SAMPLES namespace. Also see the exercise <dynaForm> with an Adaptor Data Model in this chapter.
Data Model Property Parameters
Property Parameter Description
ZENATTRS
List of additional attributes to apply to the control used for this property. This string should have the following form:
ZENCONTROL
Type of control used to display this property within a form; If not defined, Zen chooses the control type based on the data type of the property.
If you specify a simple class name as the value of ZENCONTROL, Zen assumes that this class is in the package %ZEN.Component. The following example specifies the class %ZEN.Component.textarea:
You can also specify a full package and class name as the value of ZENCONTROL. The package and class must reside in the same namespace as the class that is defining the ZENCONTROL parameter value. The following example specifies a custom component class:
ZENDISPLAYCOLUMN If defined, this is the name of the column used to provide a display value for SQL statements automatically generated for this property.
ZENGROUP The id of a group component that the control used for this property should be added to. This provides a way to control layout. If not defined, the control is added directly to the form.
ZENHIDDEN 1 (true) or 0 (false). If true, indicates that this is a hidden field. When the value of this field is sent to the client, it is not displayed. The default is 0 (false).
ZENLABEL Label used for this property within a form. The label text cannot contain the comma (,) character, because it is added to a comma-delimited list of labels.
ZENREADONLY 1 (true) or 0 (false). If true, this is a read-only field and cannot be edited by the user. The default for is 0 (false).
ZENSIZE The ZENSIZE parameter provides a value for the size property of a control, if the control has one. The interpretation of size depends on the HTML element created by the control. For example, for a text control, size is proportional to the number of characters displayed. This behavior is defined by HTML, not Zen.
ZENSQL If defined, this is an SQL statement used to find possible values for this property. This parameter corresponds to the sql property of the various data-driven Zen components. For details, see the Specifying an SQL Query section in the chapter “Zen Tables. ”
ZENSQLLOOKUP If defined, this is an SQL statement used to find the appropriate display value for a given logical value. This parameter corresponds to the sqlLookup property of data-driven Zen components like <dataListBox> and <dataCombo>. For details, see the <dataCombo> Logical and Display Values section in the chapter “Zen Controls.”
ZENTAB A positive integer. If specified, this overrides the (1–based) default tab order of the control used to display the property within a form. All controls with ZENTAB specified are placed before controls that do not define it.
ZENTITLE Optional popup title string displayed for this property within a form.
Value Lists and Display Lists
Some of the fields in a form associated with a data model might need separate value lists and display lists. Both are lists of strings. The value list gives the logical values for storage on the server, and the display list specifies the choices that the application displays to the user on the client. These concepts apply to an MVC data model as follows:
These controls (or in the case of <dynaForm>, the controls that Zen automatically generates to represent these properties) show their display lists on the client. The data model always converts values to the display format before sending them to the client. If the Zen application is not localized, the client-side value list and display list are both the same: they are identical to the server-side display list. Any client-side logic for this control must expect these values.
If the Zen application is localized into multiple languages, and if the data model class correctly defines the DOMAIN class parameter, then the conventions are a bit different. The client-side value list is still the same as the server-side display list, but now the client-side display list consists of the server-side display values in the local language. Any client-side logic for this control must expect these values.
As an example, suppose a property in an MVC data model class uses VALUELIST and DISPLAYLIST as follows:
Property Sex As %String(VALUELIST=",1,2", DISPLAYLIST=",Male,Female");
In this case, the logical value of Sex is 1 or 2. This is what is stored in the database and this is what server-side logic uses. An MVC form only sees the display values. Specifically it sees something like this:
radioSet.valueList = "Male,Female"
radioSet.displayList = $$$Text("Male,Female")  
Note:
For more about localization, the DOMAIN parameter, and $$$Text macros, see the Zen Localization chapter in Developing Zen Applications.
Object Data Model Callback Methods
When you create an object data model, you subclass %ZEN.DataModel.ObjectDataModel and provide implementations for its server-side callback methods. The following table describes these methods in detail. You first encountered several of these methods in the exercise Constructing a Model during Step 2: Object Data Model. For these methods the Example column contains the word “Yes.”
Object Data Model Callback Methods
Method Example This Callback Method is Invoked When...
%OnDeleteModel The data model is deleted. This method is implemented by the subclasses of the data model class, if they exist.
%OnDeleteSource Yes The data model is deleted. If implemented, it is responsible for deleting the object that has the given id and returning the status code resulting from that operation.
%OnGetPropertyInfo The %GetPropertyInfo method invokes it. See the discussion following this table.
%OnInvokeAction A user-defined, named action is invoked on this model object. See the discussion following this table. This method is implemented by the subclasses of the data model class, if they exist.
%OnLoadModel Yes Zen does the actual work of loading values from the data source into the data model object. The only data to load is the data that is actually seen by the user. This is the place to perform any aggregation or other operations on the data before storing it.
%OnNewSource Yes A data model needs a new instance. If implemented, it opens a new (unsaved) instance of the data source object used by the data model, and return its reference.
%OnOpenSource Yes A data model is opened. If implemented, it opens an instance of the data source object used by the data model, and returns its reference
%OnSaveSource Yes The data model is saved. If implemented, it is responsible for saving changes to the data source. It saves the given source object and return the status code resulting from that operation. Before returning the status code, it sets the data model’s %id property to the identifier for the source object.
%OnStoreModel Yes Zen does the actual work of copying values from the data model to the data source. This method loads data from the model (probably changed by the user through a form) back into the source object.
%OnSubmit A form connected to this data model is submitted. The contents of this data model are filled in from the submitted values before this callback is invoked. Implementing this callback is optional.
Virtual Properties
When a data controller needs to find information about the properties within a data model, it calls the data model’s %GetPropertyInfo method. This returns a multidimensional array containing details about the properties of the data model. The code that assembles this information is automatically generated based on the properties, property types, and property parameters of the data model class.
A data model class can modify the property information returned by %GetPropertyInfo by overriding the %OnGetPropertyInfo callback method. %GetPropertyInfo invokes the %OnGetPropertyInfo immediately before it returns the property information. %OnGetPropertyInfo receives, by reference, the multidimensional array containing the property information. %OnGetPropertyInfo can modify the contents of this array as it sees fit. Properties can be added, removed, or have their attributes changed. Attributes that you add using this method are called virtual properties. In order to use virtual properties, you must ensure that the data model class parameter DYNAMICPROPERTIES is set to 1 (true).
Note:
For examples, see the exercise <dynaForm> with an Adaptor Data Model during Step 4: Virtual Properties.”
The %OnGetPropertyInfo signature looks like this:
ClassMethod %OnGetPropertyInfo(pIndex As %Integer,
                               ByRef pInfo As %String,
                               pExtended As %Boolean = 0,
                               pModelId As %String = "",
                               pContainer As %String = "") As %Status
Where:
Within the %OnGetPropertyInfo method, the property information array pInfo is subscripted by property name. The top node for each property contains an integer index number used to determine the ordinal position of a property within a dynamically generated form:
If you want %OnGetPropertyInfo to add a new property to a data model, simply add the appropriate nodes to the property information array. The new property is treated as a “virtual” property. That is, you can set and get its value by name even though there is no property formally defined with this name. When adding a new property in %OnGetPropertyInfo, set the top level node to the current index number and then increment the index by 1:
  pInfo("Property") = pIndex
  Set pIndex = pIndex + 1
The property information array has a number of subnodes that can be defined to provide values for other property attributes. Built-in attributes start with % and include:
Attributes that do not start with a % specify values that should be applied to a property of the control with the same name. For example, the following statement causes a dynamic form to set the label property of the control used for the MyProp property to "My Label".
Set pInfo("MyProp","label") = "This is an extra field!"
Data models and data controllers each support the %OnGetPropertyInfo method. At runtime, the order in which Zen adds controls to the generated form is as follows:
  1. Creates an initial list of controls based on the data model properties and their parameters.
  2. Modifies this list of controls by calling the data model’s %OnGetPropertyInfo method, if present.
  3. Further modifies this list of controls by calling the data controller’s %OnGetPropertyInfo method, if present.
Controller Actions
The %OnInvokeAction callback lets you define “actions” that can be invoked on the data model via the data controller. The client can invoke an action by calling the dataController’s invokeAction method as follows:
controller.invokeAction('MyAction',data);
This, in turn, invokes the server-side %OnInvokeAction callback of the data model, passing it the name of the action and the data value. The interpretation of the action name and data is up to the application developer.
Data Model Series
The basic data model object consists of a series of name-value pairs.
Data Model with Name-Value Pairs
The name-value pairs in the data model comprise all of the properties in the data model class, minus those properties marked ZENHIDDEN, plus any properties added by %OnGetPropertyInfo, minus any properties deleted by %OnGetProperty. By default, the number of series is 1, but it could be larger. If there are multiple series in the model, conceptually it becomes a matrix.
Data Model with Data Series
You can add multiple series to the model if you write your own %OnLoadModel method, as in the SAMPLES class ZENMVC.ChartDataModel2, shown below. This example creates three series for the model and assigns values to data model properties in each of the series.
Class ZENMVC.ChartDataModel2 Extends %ZEN.DataModel.ObjectDataModel
{
Property Cars As %Integer;
Property Trucks As %Integer;
Property Trains As %Integer;
Property Airplanes As %Integer;
Property Ships As %Integer;

Method %OnLoadModel(pSource As %RegisteredObject) As %Status
{
  Set scale = 100

  #; This model has multiple data series. We set up the data series here.
  Set ..%seriesCount = 3
  Set ..%seriesNames(1) = "USA"
  Set ..%seriesNames(2) = "Europe"
  Set ..%seriesNames(3) = "Asia"

  #; Now we provide data for each property within each series.
  #; We use the %data array so that we can address multiple series.
  For n = 1:1:..%seriesCount {
    Set ..%data(n,"Cars") = $RANDOM(100) * scale
    Set ..%data(n,"Trucks") = $RANDOM(100) * scale
    Set ..%data(n,"Trains") = $RANDOM(100) * scale
    Set ..%data(n,"Airplanes") = $RANDOM(100) * scale
    Set ..%data(n,"Ships") = $RANDOM(100) * scale
    }
  Quit $$$OK
  }
}
When your data model has multiple series, if you bind the data model to a chart, the chart automatically picks up the various series, although you need to be careful with a pie chart. Series work similarly for a grid. A form can only display one series at a time, so you need to rely on the data controller attribute defaultSeries to determine which series is currently in view.
Custom Data Model Classes
%ZEN.DataModel.ObjectDataModel or %ZEN.DataModel.Adaptor are sufficient for most needs. However, sometimes a developer might want to create a special category of data model, for example to represent a global. In that case the developer must subclass %ZEN.DataModel.DataModel and implement the details of this subclass.
The following table lists methods that applications can call in order to work with a data model object. The behavior of these methods is up to the specific %ZEN.DataModel.DataModel subclass that implements them.
Custom Data Model Class Methods
Method Description
%DeleteModel Delete an instance of a data model object given an identifier value. This takes the given identifier value and uses it to delete an instance of a source object (if applicable). (If the data model object serves as both interface and data source, then the data model object itself is deleted).
%OpenModel Open an instance of a data model object given an identifier value. This takes the given identifier value and uses it to find an instance of a source object (if applicable) and then copies the appropriate values of the source object into the properties of the data model object. (If the data model object serves as both interface and data source, then this copying is not carried out).
%SaveModel Save an instance of a data model object. This copies the properties of the data model object back to the appropriate source object (if applicable) and then asks the source object to save itself. (If the data model object serves as both interface and data source, then the data model itself is saved.).