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

The next three sections in this chapter expand the discussion of each part of the previous figure:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

%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.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:

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.

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.

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

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

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

ClassMyApp.PatientModelExtends%ZEN.DataModel.ObjectDataModel{PropertyNameAs%String;PropertyCityAs%String;///Load an instance of a new (unsaved) source object for this DataModel.Method%OnNewSource(OutputpSCAs%Status={$$$OK})As%RegisteredObject{Quit##class(ZENDemo.Data.Patient).%New()}///Save instance of associated source object.Method%OnSaveSource(pSourceAsZENDemo.Data.Patient)As%Status{SettSC=pSource.%Save()If$$$ISOK(tSC)Set..%id=pSource.%Id()QuittSC}///Load an instance of the source object for this DataModel.Method%OnOpenSource(pIDAs%String,pConcurrencyAs%Integer=-1,OutputpSCAs%Status={$$$OK})As%RegisteredObject{Quit##class(ZENDemo.Data.Patient).%OpenId(pID,pConcurrency,.pSC)}///Delete instance of associated source object.ClassMethod%OnDeleteSource(pIDAs%String)As%Status{Quit##class(ZENDemo.Data.Patient).%DeleteId(pID)}///Do the actual work of loading values from the source object.Method%OnLoadModel(pSourceAsZENDemo.Data.Patient)As%Status{Set..Name=pSource.NameSet..City=pSource.Home.CityQuit$$$OK}///Do the actual work of storing values into the source object.Method%OnStoreModel(pSourceAsZENDemo.Data.Patient)As%Status{SetpSource.Name=..NameSetpSource.Home.City=..CityQuit$$$OK}}

The id attribute does not affect the binding but becomes useful in a future step, when we save the form.

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:

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:

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:

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:

ClientClassMethodCityIsValidJS(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.

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.

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.

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.

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:

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.

In XData Contents, place a <dataController> and <dynaForm> inside <page>:

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:

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:

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.

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

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

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.

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:

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:

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:

These statements add a <checkbox> and a <textarea> to any <dynaForm> generated by the model.

ClassMethod%OnGetPropertyInfo(pIndexAs%Integer,ByRefpInfoAs%String,pExtendedAs%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.SetpIndex=pIndex+3#; add a field at the end of the formSetpInfo("Extra")=pIndexSetpInfo("Extra","%type")="checkbox"SetpInfo("Extra","caption")="Extra!"SetpInfo("Extra","label")="This is an extra checkbox."SetpIndex=pIndex+1#; add another field at the end of the formSetpInfo("Comments")=pIndexSetpInfo("Comments","%type")="textarea"SetpInfo("Comments","caption")="Please enter additional comments:"SetpIndex=pIndex+1Quit$$$OK}

Recompile the Person class.

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:

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.ObjectDataModel 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.Adaptor interface. Then you can use the resulting class directly as the modelClass for the <dataController> component in your page class.

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:

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.

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:

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

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 <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:

PropertySexAs%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:

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.

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.

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

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:

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.

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

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:

Creates an initial list of controls based on the data model properties and their parameters.

Modifies this list of controls by calling the data model’s %OnGetPropertyInfo method, if present.

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:

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.

ClassZENMVC.ChartDataModel2Extends%ZEN.DataModel.ObjectDataModel{PropertyCarsAs%Integer;PropertyTrucksAs%Integer;PropertyTrainsAs%Integer;PropertyAirplanesAs%Integer;PropertyShipsAs%Integer;Method%OnLoadModel(pSourceAs%RegisteredObject)As%Status{Setscale=100#; This model has multiple data series. We set up the data series here.Set..%seriesCount=3Set..%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.Forn=1:1:..%seriesCount{Set..%data(n,"Cars")=$RANDOM(100)*scaleSet..%data(n,"Trucks")=$RANDOM(100)*scaleSet..%data(n,"Trains")=$RANDOM(100)*scaleSet..%data(n,"Airplanes")=$RANDOM(100)*scaleSet..%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.

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.

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

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

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