Skip to main content

Model View Controller

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:

  • Create a form that displays a set of properties, loaded from a persistent object within the database. The form automatically displays controls appropriate to the data type of each property.

  • Display a chart based on values within a form. The chart automatically updates its display whenever the user submits any changes to the form.

  • Display meters representing values calculated on the server. When the page is refreshed, these meters automatically update themselves with current values from the server.

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
generated description: mvc

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.DataModelOpens in a new tab. A data model can:

  1. Retrieve data values from one or more sources, such as:

    • A persistent Caché object

    • An external database (ODBC or JDBC) via the Caché SQL Gateway

    • A Caché global

    • An Ensemble business service

  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.ObjectDataModelOpens in a new tab and %ZEN.DataModel.AdaptorOpens in a new tab, as shown in the following figure.

Data Model Classes
generated description: mvc classes model

%ZEN.DataModel.ObjectDataModel

A subclass of %ZEN.DataModel.ObjectDataModelOpens in a new tab 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.

generated description: mvc object

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.ObjectDataModelOpens in a new tab, 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.AdaptorOpens in a new tab 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.

generated description: mvc adaptor

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.AdaptorOpens in a new tab. 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.AdaptorOpens in a new tab, 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.dataControllerOpens in a new tab. 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
generated description: mvc classes components

<dataController>

To provide a data controller for a Zen page, simply add a <dataController> or a subclass of %ZEN.Auxiliary.dataControllerOpens in a new tab 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.booleanOpens in a new tab. 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.dataControllerOpens in a new tab 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.booleanOpens in a new tab. See “Zen Attribute Data Types.”

<dataController> Methods

A <dataController> or a subclass of %ZEN.Auxiliary.dataControllerOpens in a new tab 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.dataControllerOpens in a new tab 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.dataViewOpens in a new tab 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.dataControllerOpens in a new tab object via the following properties of the %ZEN.Component.dataViewOpens in a new tab interface:

  • controller — Use in JavaScript code that runs on the client side

  • %controller — Use in ObjectScript, Caché Basic, or Caché MVBasic code that runs on the server side

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.MVCChartOpens in a new tab and ZENMVC.MVCMetersOpens in a new tab. The class ZENMVC.MVCMetersOpens in a new tab uses the same data controller to provide values for a <dynaGrid> and several meters. The class ZENMVC.MVCChartOpens in a new tab 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.clsOpens in a new tab

http://localhost:57772/csp/samples/ZENMVC.MVCMeters.clsOpens in a new tab

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.MVCChartOpens in a new tab.

generated description: mvc charts

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.clsOpens in a new tab

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:

  • %ZEN.DataModel.ObjectDataModelOpens in a new tab or %ZEN.DataModel.AdaptorOpens in a new tab

    An object data model takes more work to code. An object data model gives you explicit control over which properties end up in the model, so it makes more sense for cases when you want to shield certain properties from display or when you want to display charts, which require very fine data control. The choice of adaptor data model is simple and convenient, but it imposes a burden on the original persistent object, in that you must change its class code to allow it to become an adaptor data model.

  • <form> or <dynaForm>

    A <form> is a good choice for the key forms that are critical to the success of your application. A <form> requires more work to encode, but provides as fine a level of control as you need to perfect the results. A <dynaForm> provides automatic results and automatically updates its layout if you change the underlying data model. This is useful to generate forms for large volumes of detailed information such as system administration, inventory, or maintenance requests.

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.ObjectDataModelOpens in a new tab.

First, define an object data model class called PatientModel that knows how to load and store properties from the Patient object:

  1. Start Studio.

  2. Choose File > Change Namespace or F4.

  3. Choose the SAMPLES namespace.

  4. Choose File > New or Ctrl-N or the generated description: studio new icon.

  5. Click the General tab.

  6. Click the Caché Class Definition icon.

  7. Click OK.

  8. For Package Name enter:

    MyApp

  9. In the Class Name field, type:

    PatientModel

  10. Click Next.

  11. Choose Extends

  12. Click Next and enter (or browse to) this class name:

    %ZEN.DataModel.ObjectDataModel

  13. Click Finish.

    Studio creates and displays a skeletal object data model class:

    Class MyApp.PatientModel Extends %ZEN.DataModel.ObjectDataModel
    {
    
    }
    
  14. 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.ObjectDataModelOpens in a new tab 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
      }
    }
  15. Choose Build > Compile or Ctrl-F7 or the generated description: studio compile 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:

  • id is the unique identifier of the data controller on the Zen page. A data view (such as a form) specifies its data controller using this id.

  • modelClass is package and class name of the data model class. The previous exercise, “Constructing a Model,” created the class MyApp.PatientModel which is an object data model that represents objects of the ZENDemo.Data.PatientOpens in a new tab class.

  • modelId is a number that identifies the record to initially load from the data source. In this case, it is the identifier of a specific ZENDemo.Data.PatientOpens in a new tab object.

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 generated description: studio compile icon.

  3. Choose View > Web Page or the generated description: studio webpage 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.

    generated description: mvc form text

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.PatientOpens in a new tab 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.MVCFormOpens in a new tab in the SAMPLES namespace. Try entering the following URI in the browser:

http://localhost:57772/csp/samples/ZENMVC.MVCForm.clsOpens in a new tab

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.dataControllerOpens in a new tab 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.PatientOpens in a new tab 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:

  • Add a text field that allows the user to enter an ID

  • Add a combo box that allows the user to choose the record by one of its properties, such as a patient name

  • Add a table that allows the user to click on a patient name and see the details on a form below

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 generated description: studio compile icon.

  5. Choose View > Web Page or the generated description: studio webpage icon.

    The following figure shows the form.

    generated description: mvc form id save

  6. Try the onblur functionality as follows:

    • Enter 2 (or any other number) in the ID field.

    • Press the Tab key to move out of the ID field.

    • The alternate record should display.

  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:

    • Note the number of the current record.

    • Make a significant change to the Name or City field.

    • Click Save.

    • Enter a different number in the ID field.

    • Press the Tab key to move out of the ID field.

    • The alternate record should display.

    • Enter the number for the record that you saved in the ID field.

    • Press the Tab key to move out of the ID field.

    • Your saved changes should be visible on the form.

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 generated description: studio compile icon.

  5. Choose View > Web Page or the generated description: studio webpage icon.

    The following figure shows the form.

    generated description: mvc form full

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 generated description: studio compile icon.

  4. Choose View > Web Page or the generated description: studio webpage 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.

    generated description: mvc form text dyna

    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:

    • controllerId identifies the data controller.

    • defaultGroupId identifies the group, on the form, that contains the controls generated by that data controller.

    • defaultGroupId is optional; but if it appears in the <dynaGroup> then somewhere inside the <dynaForm>, you must specify a group with an id that matches the defaultGroupId, in this case a <vgroup>.

    • If you want controls to appear on the form that do not depend on the data controller, such as the control whose value is based on the %id variable, you must place them explicitly, as shown.

  4. Choose Build > Compile or Ctrl-F7 or the generated description: studio compile icon.

  5. Choose View > Web Page or the generated description: studio webpage icon.

    The following <dynaForm> displays:

    generated description: mvc form final

  6. You may assign specific labels to the generated controls on the <dynaForm> as follows:

    • In Studio, open the object data model class, MyApp.PatientModel.

    • Edit the properties to add the ZENLABEL parameter to each of them:

      Property Name As %String(ZENLABEL = "Patient Name");
      

      And:

      Property City As %String(ZENLABEL = "Patient City");
      
    • Choose Build > Compile or Ctrl-F7 or the generated description: studio compile icon to compile the data model.

  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. Choose Tools > Copy Class.

  3. The Class Copy dialog displays. Enter:

    • Copy ClassMyApp.PatientModel

    • ToMyApp.EmployeeModel

    • Select the Replace Instances of Class Name check box.

    • Click OK.

    The new class definition for MyApp.EmployeeModel displays in Studio.

  4. Edit MyApp.EmployeeModel so that it has the following three properties only:

    Property Name As %String;
    Property Salary As %Numeric;
    Property Active As %Boolean;
    
    
  5. 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
    }
  6. Choose Build > Compile or Ctrl-F7 or the generated description: studio compile icon.

  7. Choose Tools > Copy Class.

  8. The Class Copy dialog displays. Enter:

    • Copy ClassMyApp.MyNewPage

    • ToMyApp.MyOtherPage

    • Select the Replace Instances of Class Name check box.

    • Click OK.

    The new class definition for MyApp.MyOtherPage displays in Studio.

  9. 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.

  10. Change the <dataController> modelClass to "MyApp.EmployeeModel".

  11. Choose Build > Compile or Ctrl-F7 or the generated description: studio compile icon.

  12. Choose View > Web Page or the generated description: studio webpage icon.

    The following figure shows the resulting form, with the values for record 5.

    generated description: mvc form dyna

    <dynaForm> has chosen controls for this form as follows:

    • <text> for the Name, which is a %String

    • <text> for the Salary, which is %Numeric

    • <checkbox> for the Active status, which is a %Boolean

<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
%ArrayOfDataTypesOpens in a new tab See “Array Data Types” following the table. <textarea>
%BooleanOpens in a new tab <checkbox>
%DateOpens in a new tab In YYYY-MM-DD format <dateSelect>
%DateOpens in a new tab In other formats <text>
%Enumerated Using a VALUELIST with 4 or fewer values. <radioSet>
%Enumerated Using a VALUELIST with more than 4 values <combobox>
%ListOfDataTypesOpens in a new tab See “List Data Types” following the table. <textarea>
%NumericOpens in a new tab <text>
Object reference <dynaForm> generates an SQL query <dataCombo>
Stream %CharacterStreamOpens in a new tab <textarea>
Stream %BinaryStreamOpens in a new tab <image>
%StringOpens in a new tab With MAXLEN over 250 <textarea>
%StringOpens in a new tab 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 %StringOpens in a new tab 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 %ListOfDataTypesOpens in a new tab, 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 %ListOfDataTypesOpens in a new tab 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 %ArrayOfDataTypesOpens in a new tab, the conventions are the same as for %ListOfDataTypesOpens in a new tab, 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.ObjectDataModelOpens in a new tab, 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.AdaptorOpens in a new tab. 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.AdaptorOpens in a new tab 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.MVCDynaFormOpens in a new tab, ZENMVC.PersonOpens in a new tab, and ZENMVC.AddressOpens in a new tab in the SAMPLES namespace.

  1. Edit a persistent class so that it also extends %ZEN.DataModel.AdaptorOpens in a new tab, 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.AdaptorOpens in a new tab 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 generated description: studio webpage icon.

    <dynaForm> generates the appropriate controls and displays the form, for example:

    generated description: dynaform 1

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:

    generated description: dynaform 2

    The data model parameters have the following effects:

    • ZENATTRS applies the specified attribute(s) and value(s) to the generated control.

    • ZENHIDDEN hides the control associated with that property.

    • ZENLABEL replaces the default label (the property name) with a custom string.

    • ZENREADONLY displays the control but prevents the user from editing its contents.

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 generated description: studio webpage 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:

    generated description: dynaform 3

    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.AdaptorOpens in a new tab 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. Choose Class > Override.

  3. Select %OnGetPropertyInfo and click OK.

    Studio adds a skeleton %OnGetPropertyInfo method to the class.

  4. 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
    }
    
  5. Recompile the Person class.

  6. Refresh your view of the Zen page class in the browser.

    <dynaForm> generates the appropriate controls and displays the form, for example:

    generated description: dynaform 4

Data Model Classes

The basic behavior of data models comes from the abstract base class %ZEN.DataModel.DataModelOpens in a new tab. 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:

  • The data model class serves as a wrapper for an independent data source. The data model object provides the interface and the source object (or objects) provide the actual data. In this case the data model class must implement the additional callback methods related to the data source object. Our form examples followed this convention by subclassing %ZEN.DataModel.ObjectDataModelOpens in a new tab and overriding several server-side callback methods.

  • The data model class is also the data source object. The data model object provides both the data and the interface. In this case, there is no need to override the callback methods related to the data source object. All you need to do is to make your data source class implement the %ZEN.DataModel.AdaptorOpens in a new tab interface. Then you can use the resulting class directly as the modelClass for the <dataController> component in your page class.

Data Model Class Properties

The %ZEN.DataModel.DataModelOpens in a new tab class provides the following properties:

  • String that identifies the currently active instance of the data model object, also known as the model ID. This value can be initially set by providing a modelId attribute in the <dataController> definition. The exact form and possible values of the model ID are up to the developer of a specific data model class. The property names are:

    • dataModelId — Use in JavaScript code that runs on the client side

    • %id — Use in ObjectScript, Caché Basic, or Caché MVBasic code that runs on the server side

    For examples, see “Adding Behavior to the <form>” in this chapter.

  • Array of strings that provide display names for the data series in the model. Each string labels one data series. The array is subscripted by series number (1–based). The property names are:

    • seriesNames — Use in JavaScript code that runs on the client side

    • %seriesNames — Use in ObjectScript, Caché Basic, or Caché MVBasic code that runs on the server side

  • Number of data series contained within the data model. The property names are:

    • seriesCount — Use in JavaScript code that runs on the client side

    • %seriesCount — Use in ObjectScript, Caché Basic, or Caché MVBasic code that runs on the server side

    For details, see the section “Data Model Series.”

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.ObjectDataModelOpens in a new tab 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.ObjectDataModelOpens in a new tab 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.objectModelParametersOpens in a new tab defines these parameters; the following table lists them.

Note:

For examples, use Studio to view the classes ZENMVC.FormDataModelOpens in a new tab and ZENMVC.FormDataModel2Opens in a new tab 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:

"attribute:value|attribute:value"

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.textareaOpens in a new tab:

ZENCONTROL = "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:

ZENCONTROL = "MyPackage.Utils.textAreaAppendable"

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:

  • If you are using a <form> with a data model, this concept applies to any of the controls that offer valueList and displayList attributes: <radioSet>, <select>, or <combobox>. For details, see the chapter “Zen Controls.”

  • If you are using a <dynaForm> with a data model, this concept applies to any property whose data type uses VALUELIST and DISPLAYLIST class parameters, for example a property of type %Enumerated. For a table that matches data types with the <dynaForm> controls they generate, see the “<dynaForm> Controls Based on Data Types” table earlier in this chapter.

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.ObjectDataModelOpens in a new tab 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:

  • pIndex is the index number that should be used to add the next property to the list.

  • pInfo is a multidimensional array containing information about the properties of this data model.

  • If pExtended is true, then complete information about the properties should be returned; if false, then only property names need be returned (applications can simply ignore this).

  • pModelId a string that identifies the currently active instance of the data model object, also known as the model ID. This is provided for cases where the contents of a dynamic form may vary by instance of the data model object. The exact form and possible values of the model ID are up to the developer of a specific data model class.

  • If this is an embedded property, pContainer is the name of the property that contains it.

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:

  • pInfo("Property","%type") = "control"

    The %type subnode identifies the name of the Zen control that should be used for properties of this type when using a dynamic form. Note that this name is the class name of a component. If no package name is provided, it is assumed that this is a component in the %ZEN.Component package. Use a full class name if you wish to specify a component from a different package.

  • pInfo("Property","%group") = "groupId"

    The %group subnode indicates the id of a group component contained by a dynamic form. If %group is specified and there is a group with this id, then the control for this property is created within this group. This provides a way to control the layout of controls within a dynamic form.

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
generated description: mvc data model

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
generated description: mvc data series

You can add multiple series to the model if you write your own %OnLoadModel method, as in the SAMPLES class ZENMVC.ChartDataModel2Opens in a new tab, 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.ObjectDataModelOpens in a new tab or %ZEN.DataModel.AdaptorOpens in a new tab 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.DataModelOpens in a new tab 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.DataModelOpens in a new tab 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.).
FeedbackOpens in a new tab