TOC PREV NEXT INDEX

Using AVS/Express




15 Application Design Issues


The information in this chapter provides guidelines for using AVS/Express projects effectively. It encourages the development of projects and application networks in a consistent and orderly way so that you can easily maintain your application as it grows and maximize your reuse of objects.

If you follow these guidelines when you develop your project, you should find it easy to add to your project, to share your projects and modules with other AVS/Express users, and to include objects constructed by other users in your projects.

Note that the term "project" has a very specific meaning in AVS/Express. See Chapter 10, Projects and Processes for basic information about AVS/Express projects.

This chapter discusses application design issues. It includes the following sections:

15.1 Constructing Macros

This section discusses various tips and strategies for constructing macros that are efficient, robust, and reusable.

Using Modular Programming Techniques

A primary goal in developing application networks is to design your object hierarchy so that it is composed of modular, reusable objects. To do so, you follow the same principles that apply to writing modular programming code:

These features can be quite useful. You may want to make invisible subobjects that you do not want users to modify. And you may want to display only those connections that are important to your users.

However, keep in mind that hiding information in the Network Editor can be misleading. In particular, not displaying connections between two objects can make your networks harder to follow in the Network Editor. If possible, keep network connections visible.

Building Reusable Objects

Design your objects in a way that makes them generic and, therefore, reusable. In particular, do not refer to external objects from within a template object. If you do so, you cannot use that template object in a different context.

Thus, you should build objects that are self-contained. An application will then consist of a series of statements instantiating these template objects and setting up connections between them. The template object may contain internal references -- connections between objects inside a macro, for example -- but will not contain references to objects outside the template. This makes your templates more reusable since they are not dependent on their context. See Object->Load Objects on page 5-26 for more information.

For application developers, it is important to think about where the data is stored. Application developers should explicitly maintain data structures that need to be managed as part of the application.

For example, in a radiation treatment planning application, there are the concepts of "patient" and "plan". In an AVS/Express application, a patient is really a collection of state from various functional components. The application developer should maintain all of the information that comprises a patient in a single data structure. This would typically be a (potentially nested) group. Managing the data in this fashion will make operations such as "save patient" and "load patient" much easier to implement.

One key corollary of this point: avoid storing application state values in User Interface objects. Application state values should always be stored explicitly in a structure created by the developer; user interface objects should refer to the data structure members. If the application is implemented in the opposite fashion, whereby the data structure refers to the user interface object, it is very difficult to separate or redesign the user interface.

For example, consider the following V-code:

Template:

group PatientState {
string name;
};

Application:

UItext patient_name_text;
PatientState patient_state {
name => patient_name_text.text;// not recommended
};

This arrangement is not recommended. It will be difficult in the future to change the user interface for this application, since the value of the "name" member of patient_state is a reference to a subobject of a user interface object. In addition, if the user interface object is deleted, the name subobject of patient_state become invalid.

The following V-code represents a better approach (using the same template for PatientState as above):

UItext patient_name_text {
text => patient_state.name;
}

In this design, the user interface can be connected to and disconnected from the patient name subobject at will, without disrupting the current value or leaving the value undefined when the user interface is disconnected or removed.

See Chapter 7, The User Interface("UI") Toolkit of the Using the Toolkits manual for more details.

Structuring Objects in Hierarchical Layers

The Data Visualization (DV) modules are a good example of how to structure objects in hierarchical layers. It is recommended that you follow this model when designing your modules and macros. This model provides flexibility and consistency to the users of your objects.

The DV macros are constructed in three layers. The lowest-level object is called a primitive. This is an object of type module or group (depending on whether it has procedural code "behind" it) whose inputs and outputs are precisely the type needed for the module to perform its function. There is no user interface at this level. The module directly exports the ports for its input and output data and its parameters. Many visualization primitives operate on only part of a Field. For example, the DVthreshold primitive operates only on the values of a Field's Node_Data. This module does not affect the Field's Mesh, and therefore specifies its input as not requiring that a Mesh be present.

The second-level DV objects are referred to as Visualization macros. They can be found in the Network Editor in the Macros library on the Visualization page. These macros are completely functional modules, but do not include a user interface. The macros serve two purposes:

The Visualization Macros each have names that begin with an upper-case letter. An example of a Visualization Macro is Threshold.

The top-level DV objects are the macros that appear on the Main library page, in the Filters and Mappers libraries. These macros all have names that begin with a lower-case letter. They add a user interface (neatly contained in its own macro) to the second-level macro and contain a parameter group where the actual values (not references) of the module's parameters are stored. The user interface macro contains a group reference to the parameter block (as does the second-level macro).

Hierarchical Module Example

The DV Kit's threshold function provides a good example of a hierarchical object design.

At the lowest level is the DVthreshold primitive. It is a module that accepts Node_Data as input and produces the same as output:

Figure O-1


The input to DVthreshold is declared as Node_Data-not Field or Mesh+Node_Data- because the threshold algorithm does not care about the mesh; it operates only on the node data.

The Threshold macro (notice the upper-case T in its name) is the mid-level macro. It contains an instance of the low-level threshold primitive module and a parameter block group reference:

Figure O-2


Notice that it also adds a link and the DVcomb_mesh_and_data module so that it inputs and outputs a complete field and not just Node_Data.

The threshold macro (notice the lower-case t in its name) is the top-level macro. It contains an instance of the mid-level Threshold macro; a parameter block group, ThreshParam, containing the actual control parameters (not references to them); and ThresholdUI, a macro containing the user interface for this module.

Figure O-3


The macro also adds in obj, a GD Object as a convenience. This allows the user of this macro to connect its output directly into a viewer, without needing to find and instantiate the GD Object.

This three-level organization has the following benefits:

Making Use of Templates

You are encouraged to create and use template objects. Your application network should consist of a small number of lines of code that instantiate and connect object templates. This means that the template libraries can and should be used for objects that are not strictly template objects. Storing parts of your application as templates is analogous to using subroutines in programming code that you write. Rather than writing one big function, you break it down into smaller pieces. You store pieces of your macros as templates so that a single macro does not become too unwieldy.

For example, here is part of the V-code definition for DataObject:

DefaultMinMax MinMax<NEx=11,NEy=187,export_all=1> {
input => in;
};
DefaultLinear Datamap<NEy=231,NEx=11,export_all=1> {
dataMin => MinMax.min_value;
dataMax => MinMax.max_value;
};
DefaultProps+OPort Props<NEy=198,NEx=297,export_all=1>;
DefaultModes+OPort Modes<NEy=165,NEx=341,export_all=1>;
DefaultTexture+OPort Texture<export_all=1> {
data => texture_in;
colormap => texture_col_in;
};
DefaultPickInfo+OPort PickInfo<export_all=1>;
AltObject AltObject<export_all=1> {
alt_in => in;
};
DefaultObject+OPort Obj<NEy=385,NEx=110,export_all=2> {
...

Notice how most of the subobjects of DataObject are simply instances of some object template (named Defaultxxx).

Storing parts of your application as templates improves the maintainability of your network and allows for easier file management (since V-code can be stored in multiple files, rather than in one large file).

Constructing Objects with a Common Interface

Another approach to creating reusable objects is to create a common template (superclass) to define objects with a common interface. This also allows you to create and delete object connections dynamically.

For example, suppose that your application consists of a module that reads data, executes some user-specified visualization technique on the data and renders the output. Your application network might consist of the following modules:

Figure O-4


The three AVS/Express visualization modules all have a common interface: they accept a Field as input and output a Field and a DataObject. Because of this, you can dynamically (pro-grammatically) connect and disconnect the reader and viewer to the visualization module specified by the user. Note that you must be careful using this approach with DV modules, since they do not free their outputs on deinstance. Delete these objects rather than deinstancing them to free up memory.

Dynamic instancing/deinstancing of objects can be accomplished with the instancer module (in Accessories.Utility_Modules.General). Dynamic creation/deletion of objects can be accomplished via user-provided control modules that call OMcreate_obj_from_path, OMuser_destroy_obj or other similar routines.

15.2 Managing Projects and Files

This section discusses various techniques and strategies for managing your projects, objects, and V-files.

templ.v

The default location for storage of additional templates that you add to AVS/Express is the templ.v file. Changes are recorded here when you perform a Project->Save operation.

Just as you want to keep your top-level application network specification clean and simple, the same is true for templ.v. We recommend that you do not store all of your template definitions directly in templ.v. If you do so, it should be clear that templ.v will quickly become large and difficult to manage. In addition, if multiple developers are working on the same project, you will all have to share and manage templ.v.

Rather than specify all your object definitions in templ.v, group them into different library files. templ.v will then contain references to these files. For example, here is a small part of AVS/Express's default templ.v file:

"gdif" GDIF;
"fld" FLD;
"fldf" FLDF;
"ren" REN;
"fld_meth" DV_FLD;
"gmod" GMOD;
"file" FILE;
"pal" PAL;
"ip_pkg" IP_PKG;

Each of these lines of V-code is a reference to a library. The first line specifies a library named GDIF whose objects are defined in the file gdif.vo. (The syntax used will be discussed in the next section.)

Project->Save vs. Object->Save Objects

You may be used to saving the state of your project's templates by using the Project->Save function in the Network Editor. Project->Save causes the system to write the current definition of your template objects to your project templ.v file. You can see that if you do not want the definitions of all of your template objects to be specified in templ.v, you will not want to use Project->Save.

Rather than use Project->Save, select the library you have changed and execute a Save Objects (from the Object pulldown menu) on it. Do this for each library you have changed. This will cause the definition of this library to be written out to the specified file. As you make updates to libraries, you will probably overwrite the existing library .v file with the new definition. We recommend that you always keep a backup file for each library so that you do not accidentally overwrite a file you that wanted to save.

There is one case to which you should pay particular attention. This is when you have template object definitions stored in V files and you edit that object in an application area such as DefaultApplication or SingleWindowApp.

When you instantiate an object in an application area, the object is defined relative to its template. For example, this is the V-definition of the add_num template object (located in Library_Workspaces.User_Functions):

module add_num {
float+read+req src_1<NEportLevels={2,0}>;
float+read+req src_2<NEportLevels={2,0}>;
float+write+nonotify res<NEportLevels={0,2}>;
omethod+notify_inst+notify+req update = "USERadd_num";
};

Suppose you have this definition stored in the file add_num.v.

If you instantiate the add_num module, this is its definition in the application area:

USER.add_num add_num;

If you edit the add_num object in the application area, the changes are saved relative to the object template. For example, if you add a byte to add_num:

USER.add_num add_num {
byte byte<NEportLevels={1,1},NEx=517.,NEy=88.>;
};

Now, suppose you want to save these changes to a V file. If you perform a Save Objects and specify that you want to overwrite add_num.v with this definition, you will have wiped out the definition of add_num stored in that file. You will wind up with just your relative changes, without the definition of the full object to which it refers. Obviously, you want to avoid doing this.

One way to avoid accidentally overwriting your template definition is to edit template objects in the ScratchPad or using the Object Editor (invoked directly on the template in the library). Then you are editing the actual template and not an instance of that template.

Syntax for Referring to Library Objects

In earlier versions of AVS/Express, you included library V files inside of another V file, such as templ.v, using the libfile property. Version 3.0 and later of AVS/Express contains support for an alternate syntax, the "double-quote" syntax. We recommend that you use this new syntax rather than the libfile property when referring to V files.

Use of this syntax has advantages over using the libfile property:

Note that the double quote syntax is intended for use with library objects. As such, it supports files which contain a single object only (typically, a library -- the library can contain subobjects and sublibraries).

If you specify a file that contains more than one object, the Object Manager will load the first object only and report an error.

If you refer to the library without a .v extension, the Object Manager will look for a binary V, or .vo, file. If one does not exist, or if it is out of date, the Object Manager will look for the V , or .v, file, load it, and generate a .vo file, which it will try to use the next time the file needs to be loaded.

AVS/Express' templ.v uses the double-quote library syntax. Here are the first few lines of that file:

library Templates<user_class=1>
{
"std" STD;
"stdmac" STDMAC;
"config.v" CONFIG;
};

Your templ.v might look something like this:

"$XP_PATH<0>/v/templ.v Templates {
"vizmac" Filters;
};

vizmac is the V file that contains the contents of the "Filters" library. To repeat: when the OM encounters this statement, it looks first for the file vizmac.vo. If it does not find it, it looks for vizmac.v. If it finds vizmac.v, the OM generates vizmac.vo for use the next time this library is loaded.

Organizing Objects into Libraries

It is useful to divide libraries into those that contain modules (source code) and those that contain macros-often including a user interface-that use the modules. You may also want to have a library (or libraries) that define data types. With this approach, you will always store module objects in one library V-file and macro objects in a separate library V-file.

Your templ.v file would then look something like:

$XP_PATH<0>/v/templ.v Templates {
"/usr/app/v/mods" APP_MODS;
"/usr/app/v/macs" APP_MACS;
};

mods.v contains modules:

flibrary APP_MODS {
module MOD1 {
int+notify+read a;
...
omethod
};
...
};

macs.v contains macros:

// Set NEeditable=1 on libraries whose objects are
// edited with the Network Editor

flibrary APP_MACS <NEeditable=1> {
macro MAC1 {
...
APP_MODS.MOD1 MOD1 {
...
};
};
};

The advantages of structuring your libraries this way include:

Using build_dir

It is strongly recommended that you use the build_dir property on library objects. This allows organization of related source files into common directories, and separates unrelated files. Choose a short name that is likely to be unique. Remember that AVS/Express runs on both UNIX and Windows platforms, and that many PC tools for archival, backup, and so on still cannot handle filenames longer than 8.3.

The value of the build_dir property should be related to the name of the library in a way that is obvious, so that a user examining the directory and V-code would be able to associate the two easily (more on this below).

Use of directory names that exist in the installed version of AVS/Express should be avoided. This includes the most common choice for the value of build_dir: src. Use of directory names that are already in use by AVS/Express can lead to confusing errors during compilation and linking.

The build_dir property, along with many other properties in AVS/Express that have values that are filenames or directory names, should never be set to an absolute pathname. These properties should always be set to paths that are relative to the project directory. A build_dir property with the value "foo" will result in a directory named <project_dir>/foo being created.

AVS/Express' project mechanism determines pathnames automatically, with information from the avsenv file (XP_PATH environment variable), and from build_dir and xx_src_file and other properties. The presence of absolute pathnames in object properties will only lead to unnecessary difficulty in exchanging modules and projects with other AVS/Express users.

Structuring Project Directories

It it useful to structure your directories in such a way that they mirror the V library directory hierarchy (to the library level, not the object level). For example:

Figure O-1


The top-level "libs_dir" is optional.

You can set the build_dir property on the each AVS/Express library to its corresponding directory. This directory structure is useful when working within a source control system.

Source Code Control Systems

Most users of AVS/Express do not work on an application alone, but rather, with a group of developers under the control of a source code control system. There are two similar AVS/Express project structures you might consider for working in this environment:

1. A two project hierarchy composed of the AVS/Express install project and an inherited group project under source control.
Your personal work area project is a copy of the group area project, that is, it inherits directly from the AVS/Express install and contains all additions/modifications currently under source control.
Some source control systems easily support this architecture by allowing your personal work area to consist of links to the source control directory hierarchy (so that you do not need to make a copy of all the files you need).
2. A three project hierarchy consisting of the AVS/Express install project, the group project under source control and your personal project. In this case, your XP_PATH (in the avsenv file) might look something like this:
XP_PATH=/users/auser/MyProj /net/share/APP/GroupProj/usr/express

You can use an environment variable to reference the group project:

XP_PATH=/users/auser/MyProj $GROUPPROJ /usr/express

See Chapter 5, Objects in AVS/Express for more information on nested projects.

Once your hierarchy is in place, you can:

getenv V-function

The getenv V-function is often useful when working in a group project. The getenv function allows you to access the value of environment variables within your V-code. For example:

WORKSPACE_1<process=(getenv("PROCESS"))> {
...

where PROCESS is an environment variable set to a string such as "user" or "express".

Using an environment variable and the getenv function in this way, you can respecify (from the environment) which process objects are linked into without editing V-code and without needing to make a change in multiple files.

Editing V-files

The Network Editor allows you to put together networks quickly and view the results visually. Many people develop and edit V-code using the Network Editor since they consider it the most efficient way to create objects. Some people prefer to write V-code "by hand" using a text editor.

Which way you choose is mostly a matter of personal preference. However, you should be careful about mixing these two styles of creating/editing V-files. If you create a V-file using an editor, load this V-file into the Network Editor and then save it using Object->Save Objects, you will encounter the following limitations:

For these reasons, you may want to keep two sets of (mutually exclusive) V-files -- those you edit in the Network Editor and those you edit with a text editor. You may want to include a prefix on your V-filenames to distinguish the two types of files, for example "ne_file1.v" and "ed_file1.v".

15.3 Cross-Platform Issues

There are two kinds of differences you must consider when developing a User-Interface that is run on both UNIX(Xt/Motif) and PC (Windows 95 or NT) operating systems: function and look and feel.

Functional Differences

The AVS/Express widget set is largely the same on UNIX and the PC. However, certain widgets exist on one platform and not the other. For example, UIcomboBox is found on the PC only.

Certain subobjects may not be functional on one platform. For example, remote display capability of widgets is not available on the PC so the display subobject of UIshell has no effect on that platform.

Certain behavior may be different. For example, with UIdial, a user can type a value into the text field at the top of the dial using the Motif widget but not with the PC Windows widget.

One way you can work around these issues is by using ifdefs in your V-code. This solution is not ideal, but may be effective in some cases. For example:

#ifdef MSDOS
UIcomboBox UIcomboBox {
parent => <-.listFrame;
...
};
#else
UIoptionMenu UIoptionMenu {
parent => <-.listFrame;
...
};
#endif
Cross-Platform Look and Feel

Due to differences between platforms in the look and feel of the UI, some of your V-code will produce different visual results on UNIX and PC platforms.

For example, a UItext widget may look just the way you want on a UNIX platform when you set its height to 34. This may look too big on the PC because the default font is different there and because the placement of the text within the widget varies slightly.

The following sections describe ways you can make your user interface macros more portable across platforms.

Externalizing Common Subobject Values

You can create a "master" object that controls things like font attributes. All of the widgets on a particular panel can then reference this one object.

This makes it easy to change the attribute in all widgets after the interface is constructed since you only need to make the change in one place. For example,

UIfontAttribute common_font_attribs;
widget1 {
&fontAttributes => common_font_attribs;
};
widget2 {
&fontAttributes => common_font_attribs;
};

You can also create references to different groups that are platform specific.

group UIValues { // template object
prim val1;
prim val2;
...
};
UIValues PCValues {
val1 = 18;
val2 = 100;
...
};
UIValues UNIXValues {
val1 = 10;
val2 = 86;
...
};
#ifdef MSDOS
UIValues &CurValues => PCValues;
#else
UIValues &CurValues => UNIXValues;
#endif
};
UItext MyText {
height = CurValues.val1;
};
Using Formulas to Specify Widget Geometry

Most widgets do not resize or relocate automatically. For example, if you make a shell bigger, its child panel does not automatically get bigger with it. Or, if you have a button positioned in the middle of a shell, it will not stay in the middle if the user resizes the window.

If you want this behavior, use formulas to specify the widget size and placement. Set the x and y subobjects of a User Interface object to be references whose values are relative to some other object, perhaps its parent, or perhaps a neighboring widget. For example, if you have 10 buttons in a vertical column, the y position of each button might be relative to the previous button.

Set the height and width of an object to a formula that references the clientHeight, and/or clientWidth of the parent (container) object. For example, the width of a UIpanel can be set to:

=>parent.clientWidth-10

This sets the panel's width to be 10 pixels less than the parent widget's clientWidth (that is, its usable width).

15.4 Dynamic Applications

AVS/Express can be used to implement dynamic application designs. A dynamic application is one that creates and destroys objects as it needs them or no longer needs them. When most applications start, all of their components are created automatically, whether they're being used or not. These are called static applications.

You would create a dynamic application for two main reasons:

For more information on the GMOD.instancer object, see the on-line reference page for instancer.
Example

For example you can create an ADD object which takes an object name as an input and creates that object name using OMcreate_obj.

The ADD object then connects it to a previously created Read Field object for input and its output to a previously created Uviewer3D using the OMadd_obj_ref call.

The object name can come from the user through a user interface widget as a selected string from an array of choices. Each choice represents an object created in the same format as the others, i.e. one input field and one output data object. Therefore the ADD object can add any of these choices without you needing too much knowledge of the created object's structure.

Note that you can automatically parent the user interface for the newly created object to some default shell or panel by using the hconnect object or by programmatically setting the parent id of some self-contained user interface panel. For more information on the hconnect object see the on-line reference page for hconnect.

To delete objects you can create a DELETE object which takes an object name as input and deletes that object using OMuser_destroy_obj.

IMPORTANT: It is important to note that the OM collapses sequential "identical" events. If a developer programmatically changes the same object (like a panel's parent id subobject) three times in a row, only the last change will take place. A developer can insure each of the three changes take place by enveloping the changes with OMpush_ctx and OMpop_ctx calls. For more information on the OMpush_ctx and OMpop_ctx calls see the on-line reference pages for OMpush and OMpop.
15.5 Module Design: Error Handling

When errors occur within your module code, you are responsible for handling them. There are three actions you should take:

1. Return a failure error status from the module.
2. Report the error, if needed
3. Carry out any necessary error handling and clean up, such as freeing memory, resetting variable values, or calling an error handling routine.

To report errors within your module code (item 2 in the list above), you can do one of the following:

ERRverror("CreateDates", ERR_PRINT,
"Maximum number of dates reached; no more will be added.\n");
print_error is not currently documented. Here is a description of its subobjects:
module print_error {
boolean+read+IPort2 error; // trigger: 1=print; 0=don't
string+read+IPort2 error_source; // module name
string+read+IPort2 error_message; // the error message
int+Iparam on_inst = 0; // print err on instantiation?
omethod+notify_val+notify_inst print_error = "GMODprint_error";
};
Here is a sample of print_error:
// validate the in input; make sure > 0
module foo {
int+Iparam in; // in should be > 0
GMOD.print_error print_error {
error => (in <= 0);
error_source = "foo";
error_message = "in is negative, invalid value";
};
};

TOC PREV NEXT INDEX