4.3 Repeating Structures with repeat


4.3 Repeating Structures with repeat

The ability to adapt dynamically to user interaction is one of the most important usability features of electronic forms. We saw examples of such dynamic behavior when reviewing construct switch in Section 4.2 and when creating dynamic selections via element itemset in Section 3.4.5. XForms construct repeat , described in this section, completes this picture by enabling the creation of template-based user interfaces that grow or shrink during user interaction.

Such repeating constructs are most commonly found when interacting with electronic stores on the Web today where the metaphor of a dynamic shopping cart is implemented using appropriate looping constructs in either client-side or server-side scripts. XForms construct repeat defines a declarative construct that can be used to iterate over collections of like nodes in the XForms model, for example, the items in a shopping cart.

4.3.1 Designing Construct repeat

The purpose of construct repeat is to create a user interface that repeats over a collection of nodes. To this end, repeat can be thought of as an iterator ; contents of repeat can be thought of as a template of the user interface to be created for each node in the collection being iterated. The user interface created by construct repeat displays a portion of the collection being iterated with user interaction facilities that permit the user to scroll through the collection, and to add or delete nodes to the collection. Notice that all of these operations depend on the notion of a well-defined current node in the collection; XForms calls this the repeat index . Thus, construct repeat encapsulates the following information:

Id

A unique id used to identify the repeat construct.

Collection

The collection of nodes being iterated. This is a node-set identified using an XForms binding expression. The binding expression appearing on construct repeat also establishes the context for evaluating relative XPath locators within the body of repeat .

Index

An index that points at the current node in the collection. To maintain consistency with XPath, XForms uses a 1-based index to identify the current node in the collection, which is made available to authors via XPath extension function index .

UI Template

The body of construct repeat contains a user interface template to be instantiated for each member of the collection. User interface controls appearing in this template use relative XPath locators in their binding expressions. These XPath locators use the binding expression of the containing repeat to establish the evaluation context; the current node, that is, . using XPath notation, is determined by the repeat index.

Controls

Controls that enable the user to add to, delete, or scroll the nodes in the collection. These add , delete , and scroll controls are created by including the appropriate event handler within XForms control trigger . All of these handlers use the repeat index, which they access by passing the id of the repeat to index function. Add, delete, and scroll controls may appear as part of the template user interface within element repeat ; alternatively, they may appear outside the body of construct repeat . The former design might be used to create these controls once per element of the underlying collection; the latter can be used to create a toolbar that is presented along with the repeat .

Presentation

Presentation hints that indicate the portion of the collection to display. This is achieved by specifying the index of the first node to display and the suggested number of members from the collection to display. These are only hints; the presentation must display the current node at all times.

4.3.2 Anatomy of Construct repeat

Next , we define the XML markup that allows XForms authors to create repeating user interfaces. The information items enumerated in Section 4.3.1 are encoded via appropriately designed attributes and child elements. In addition to these, construct repeat can use all the common XForms user interface markup described in this chapter.

Attributes of Construct repeat

startindex

Optional attribute startindex specifies the first member of the collection that is presented to the user. It defaults to 1 if not specified, that is, the presentation starts with the first member of the collection.

number

Optional attribute number specifies the number of members to display at any given time. This is a presentation hint, and the client can display fewer or more members of the collection as appropriate for the connecting device.

Binding

Attribute pair ( model , nodeset ) locates the collection of nodes to be iterated. Alternatively, this binding can be specified using attribute bind to address a predefined binding site .

Child Elements of repeat

The body of construct repeat is the template user interface to be used when presenting each member of the collection. This can use all of the XForms user interface vocabulary [1] in addition to markup defined by the host language. Thus, when using XForms within XHTML, body of construct repeat might use XHTML markup in addition to markup defined by XForms. Conceptually, the contents of construct repeat can be thought of as being enclosed in an anonymous group construct. This is useful in answering common styling questions that arise with respect to the presentation of repeat user interfaces.

[1] An exception to this is construct switch .

4.3.3 Shopping Cart Using Construct repeat

Using the markup described so far, we define a shopping cart example that allows the user to add and remove products from a conceptual shopping cart interactively. The visual interface for the complete shopping cart example is shown in Figure 4.9.

Figure 4.9. X-Smiles rendering of the XForms shopping cart.

graphics/04fig09.gif

The shopping cart display grows or shrinks appropriately and displays a portion of the cart that includes the current item at any given time. This example consists of two parts , the model and the user interface, each of which is described following. This shopping cart will be extended with add , delete , and scroll controls in Section 4.3.4.

Shopping Cart Model

For this example, we first define an XForms model that declares the structure of our shopping cart in Figure 4.10.

Figure 4.10 Shopping cart model defines the structure of element cart .
 <  model   xmlns  ="http://www.w3.org/2002/xforms"  xmlns:xsd  ="http://www.w3.org/2001/XMLSchema"  id  ="cart"  schema  ="cs.xsd"> <  instance   id  ="c1"> <  cart   xmlns  =""> <  line-item  ><  item  ><  product  >...</  product  ></  item  > <  quantity  >1</  quantity  >  <!-- cost includes: price, tax and shipping and will be computed -->  <  cost  /></  line-item  ></  cart  > </  instance  > <  instance   id  ="cat"> <  catalog   xmlns  =""> <  product   sku  ="a1"> <  description  /><  price  /></  product  > </  catalog  ></  instance  > <  bind   nodeset  ="/cart/line-item/cost"  calculate  ="../item/product/price *../quantity +../item/product/price *../quantity * 0.08 +../item/product/shipping"/> </  model  > 

This model uses the schema defined in Figure 4.11 to define the type and structure of the instance data used by the shopping cart. The shopping cart holds one or more line-item elements, with each line-item containing details about a given item being purchased.

Figure 4.11 Schema for the shopping cart.
 <  x:schema   id  ="cart-schema"  xmlns:x  ="http://www.w3.org/2001/XMLSchema"> <  x:element   name  ="cart"> <  x:complexType  ><  x:sequence  > <  x:element   ref  ="line-item"  maxOccurs  ="unbounded"/> </  x:sequence  ></  x:complexType  > </  x:element  > <  x:element   name  ="line-item"> <  x:complexType  ><  x:sequence  > <  x:element   ref  ="item"/> <  x:element   name  ="quantity"  type  ="x:integer"/> <  x:element   name  ="cost"  type  ="x:decimal"/> </  x:sequence  ></  x:complexType  > </  x:element  > <  x:element   name  ="item"> <  x:complexType  ><  x:sequence  > <  x:element   ref  ="product"/> </  x:sequence  ></  x:complexType  > </  x:element  > <  x:element   name  ="product"> <  x:complexType  ><  x:sequence  > <  x:element   name  ="description"  type  ="x:string"/> <  x:element   name  ="price"  type  ="x:decimal"/> <  x:element   name  ="shipping"  type  ="x:decimal"/> </  x:sequence  > <  x:attribute   name  ="sku"  type  ="x:string"/> </  x:complexType  > </  x:element  > </  x:schema  > 

Notice that this model includes two instances, the first to hold the items on the shopping cart, and the second to hold a product catalog; contrast this with the similar example on dynamic selections in Section 3.4.5 where we used separate models for the bookshelf and catalog. When using multiple instances, XForms binding expressions use XPath extension function instance to identify the instance being addressed; by default, XForms binding expressions address the first instance in the model.

Observe the following facts with respect to the model shown in Figure 4.10:

Structure

Element product in the catalog instance has the same type and structure as product in the shopping cart. This correspondence will be used to advantage during user interaction, where selection controls will be used to copy the selected product from the catalog into the shopping cart.

Constraints

Later chapters will add additional dynamic constraints to the shopping cart via XForms element bind .

Calculate

Some fields appearing in the shopping cart are computed as a function of other values. Thus, the total cost of an item might be computed from its price, tax, and cost of shipping. Such calculations are defined via model property calculate using XForms element bind described in a later chapter.

Catalog

In this example, we have shown the catalog as an inline instance. In a real-world shopping application, element instance would instead refer to the catalog via a URI. This form of indirection leads to easier maintenance of the XForms application.

Shopping Cart User Interface

The markup shown in Figure 4.12 binds an XForms user interface to the shopping cart model. We use construct repeat to iterate the line-item elements in the shopping cart. Attribute startindex is set to 1 to cause the user interface to display a portion of the cart starting with the first line-item element; attribute number is set to 3 to serve as a presentation hint. The markup appearing within body of element repeat contains XForms user interface controls for populating an individual line-item in the cart.

Figure 4.12 Shopping cart user interface using construct repeat .
 <  group   xmlns  ="http://www.w3.org/2002/xforms"> <  label  >Shopping Cart</  label  > <  repeat   id  ="cartUI"  model  ="cart"  nodeset  ="/cart/line-item"  startindex  ="1"  number  ="3"> <  select1   ref  ="item"  appearance  ="minimal"> <  label  >Select Product</  label  >  <!-- Namespace qualify bindings.-->  <  itemset   nodeset  ="instance('cat')/product"> <  label  > Item: <  output   ref  ="description"/> Price: <  output   ref  ="price"/> </  label  > <  copy   ref  ="."/> </  itemset  ></  select1  > <  input   ref  ="quantity"> <  label  >Quantity</  label  ></  input  > Price + Shipping: <  output   ref  ="cost"/> </  repeat  ></  group  > 

Control select1 is used within the body of repeat to enable the user to select a product from the catalog. We have used appearance to request a minimal presentation, that is, one that takes up minimal display real estate. A visual user agent might choose to render this control as a pull-down list. This is appropriate, since the user will be presented with this control once for each line-item appearing in the shopping cart.

Binding attributes on control select1 specify that the value selected will be placed in the shopping cart at location

 
 /cart/line-item/item 

The available products are rendered via element itemset whose binding expression refers to the catalog instance to determine the available products at run-time. Note that this binding expression uses function instance as in

 
 nodeset="instance('cat')/product" 

to address the set of products listed in the catalog. Child elements label and copy also use this binding expression to set up the XPath context for evaluating relative XPath locators.

Element itemset creates the list of alternatives as described in Section 3.4.5. Notice that in this example, we have used element output within label to build up a label for each alternative that extracts the description and price fields from the product to be selected. Finally, element copy uses binding expression

 
 ref="." 

to declare that picking one of the alternatives should result in the current node operated on by the containing itemset being copied to the location specified by the select1 . The select1 has been set up to populate the item child of the current line-item . The result of this copy operation is therefore the addition of the selected product element as a child of element item in the current line-item .

By definition, the product contained in line-item/item in the shopping cart has the same type and structure as the product appearing in the catalog; thus, the copy operation populates the shopping cart with structurally valid content.

The remainder of the template user interface in repeat is fairly simple; user interface control input allows the user to specify the quantity to be ordered. Element output is used to display the cost of this line-item. This is a computed value and is automatically recalculated when the user selects a product and specifies the quantity being ordered.

The markup shown in Figure 4.12 does not contain presentational or stylistic information. In a real-world shopping cart, this markup would be hosted in an appropriate host language, for example, XHTML; this would provide the necessary constructs for aligning the controls appearing within the body of repeat or styling them via CSS.

We will extend this example in a later chapter by extending the model to define cardinality constraints on the number of items that can be placed in the shopping cart. These constraints can then be used in enabling or disabling the add and delete controls shown in Figure 4.13. The shopping cart is contained within construct group that has an appropriate label. This group will be used next in aggregating the repeat with the add, delete, and scroll controls.

Figure 4.13 Adding add , delete , and scroll controls to shopping cart.
 <  group   xmlns  ="http://www.w3.org/2002/xforms"  xmlns:ev  ="http://www.w3.org/2001/xml-events"> <  label  >Shopping Cart</  label  > <  repeat   id  ="cartUI">...</  repeat  > <  group   model  ="cart"  ref  ="/cart"> <  label  >Shopping Cart Toolbar</  label  > <  trigger   id  ="addItem"> <  label  >Create Item</  label  > <  insert   nodeset  ="line-item"  at  ="index('cartUI')"  position  ="after"  ev:event  ="DOMActivate"/></  trigger  > <  trigger   id  ="del"> <  label  >Remove Item</  label  > <  delete   nodeset  ="line-item"  at  ="index('cartUI')"  ev:event  ="DOMActivate"/></  trigger  > <  trigger   id  ="forward"> <  label  >Scroll Forward</  label  > <  setindex   repeat  ="cartUI"  index  ="index('cartUI')+1"/></  trigger  > <  trigger   id  ="back"> <  label  >Scroll Back</  label  > <  setindex   repeat  ="cartUI"  index  ="index('cartUI')-1"/></  trigger  > </  group  ></  group  > 

4.3.4 Adding Controls to the Shopping Cart

Next, we add add , delete , and scroll controls to the shopping cart. These controls will use XForms declarative event handlers insert , delete , and setindex , respectively. We first describe these event handlers before using them within control trigger to create the desired controls.

Event Handlers for Use with repeat

Event handlers insert and delete enable the addition and deletion of nodes to a collection of nodes. In this sense, they are not specific to construct repeat ; however, in XForms 1.0 they are mostly used in conjunction with user interfaces created via repeat . Handler setindex is specific to repeat since it manipulates the index that determines the current node for a construct repeat . Invoking any of these handlers causes the user interface created via construct repeat to be updated appropriately to reflect the new state of the underlying collection.

insert and delete Handlers insert and delete are symmetric with respect to their underlying design as well as the XML markup they expose. These handlers need to specify the following items of information:

Collection

Identify the collection of nodes that will be affected by the insert or delete operation. This is encoded used XForms binding attributes, either via attribute pair ( model , nodeset ) or via a predefined binding site.

Location

The location in the collection that is to be affected by the insert or delete operation. This is specified via an XPath expression in attribute at . This expression is evaluated to give a 1-based offset into the collection. This XPath expression can use extension function index to compute an offset that is relative to the current node in the collection.

Position

When inserting nodes into a collection, we need to know whether the new node is to be inserted before or after the specified location. Handler insert uses attribute required position to encapsulate this item of information. Attribute position can be set to one of either before or after .

Eventing

Attributes defined by module XML Events used to wire up these handlers to the appropriate user interface events.

setindex Handler setindex can be viewed as a specialized assignment statement for modifying the value of the index for a given repeat construct. It takes the following items of information:

repeat

Required attribute repeat holds the id of the repeat construct to be affected.

index

Required attribute index holds an XPath expression that is evaluated to produce the new value for the repeat index. This expression can use extension function index to compute the new index value relative to the current node in the collection.

Add, Delete, and Scroll Controls

Here, we use actions insert , delete , and setindex within control trigger to create add , delete , and scroll controls for the shopping cart. For this example, we will place these controls outside the body of construct repeat to create a conceptual toolbar for the shopping cart. This toolbar will be placed inside a group that carries the relevant metadata for these controls. The toolbar and the user interface created via repeat are in turn placed inside a group to capture the fact that the toolbar and shopping cart are logically related .

Observe the following in the example shown in Figure 4.13:

Grouping

The controls are grouped using construct group . XForms binding is used to specify the XPath context to be used in resolving relative XPath locators appearing within this group .

Events

The trigger controls respond to event DOMActivate .

Location

The insert and delete handlers within the add and delete controls are set up to operate on the current line-item. This is achieved by calling extension function index with the id of the repeat construct in the value of attribute at .

 
 at="index('cartUI')" 

As a result, the add control creates a new line-item at the current position in the cart; the delete control removes the current line-item from the cart.

Position

The insert handler in the add control specifies that the new line-item should be inserted after the current line-item.

Scroll

Handler setindex is invoked by both the scroll forward and scroll back controls. Each of the contained setindex handlers has attribute repeat set to cartUI to identify the repeat construct that created the shopping cart user interface. Both scroll controls are set up to move through the shopping cart one line-item at a time by specifying a new value for index that is offset by 1 from the current index value.

 
 index="index(cartUI)+1" index="index(cartUI)-1" 

4.3.5 User Interaction with Construct repeat

We now review the various stages of user interaction with the shopping cart as created so far. The shopping cart starts off with one empty line-item when the user first accesses the application. This is because of the initial instance declared in the model shown in Figure 4.10. The line-item is empty; that is, no product is selected. The presentational hints on construct repeat in Figure 4.12 request that three line-items be displayed starting with the first line-item. Since there is only one line-item to display, the presentation starts off displaying only one line-item. The index of construct repeat is set to 1. The toolbar consisting of the add , delete , and scroll controls is rendered along with the shopping cart.

In rendering the template user interface specified within the body of construct repeat for the line-item in the cart, the XForms client creates a select1 control with no product selected and fields that bind to the quantity and cost fields. Field quantity has an initial value of 1 as specified in the model. Next, the user activates control select1 to pick a specific product using the pull-down list. Selecting a product causes the corresponding product node from the catalog to be copied to the shopping cart.

The current line-item is no longer empty. The select1 control now displays the description of the selected product. When the shopping cart model is updated with this selected product, that update in turn triggers the computation of field cost . The shopping cart model has defined the value of cost as follows :

 
 calculate="../item/product/price *../quantity +../item/product/price * ../quantity * 0.08 +../item/product/shipping"/> 

This expression is evaluated to produce the value to be stored in cost . The user interface is updated to reflect the updated value of field cost . Next, the user changes the value of quantity from 1 to 5. This again causes field cost to be recomputed and the user interface to be refreshed appropriately.

Next, the user adds a new line-item to the shopping cart by activating the add control. This invokes handler insert which clones the initial line-item node as declared in the instance element and inserts this copy into the shopping cart. Notice that each time handler insert is invoked, it will use the line-item from the initial instance when creating the new line-item to be inserted. As a result, each newly created line-item starts off as described in the initial step.

The user proceeds to add new line-item entries to the shopping cart. As the number of line-item entries grows beyond 3, the presentational hint provided via attribute number takes effect, and the user interface scrolls to display a portion of the cart that includes the current line-item . Activating the scroll forward or scroll back controls results in the index being assigned a new value; this in turn results in the user interface being updated to display the current line-item .

4.3.6 Using Construct repeat within XHTML Tables

When presenting the shopping cart user interface within an XHTML page, each line-item might be presented as a row of a table. The obvious solution would be to place element repeat within an XHTML table element and have the repeat produce the rows of the table; see Figure 4.14.

Figure 4.14 Placing element repeat within element table produces invalid XHTML.
 <  table   xmlns  ="http://www.w3.org/1999/xhtml"  xmlns:xf  ="http://www.w3.org/2002/xforms">  <!-- XHTML does not permit this -->  <  xf:repeat   id  ="cartUI"  model  ="cart"  nodeset  ="/cart/line-item"  startindex  ="1"  number  ="3"> <  tr  > <  td  > <  xf:select1   ref  ="item"  appearance  ="minimal"> <  xf:label  >Select Product</  xf:label  > <  xf:itemset   nodeset  ="instance('cat')/product"> <  xf:label  > Item: <  xf:output   ref  ="description"/> Price: <  xf:output   ref  ="price"/> </  xf:label  > <  xf:copy   ref  ="."/> </  xf:itemset  ></  xf:select1  > </  td  > <  td  > <  xf:input   ref  ="quantity"> <  xf:label  >Quantity</  xf:label  ></  xf:input  > </  td  > <  td  > Price + Shipping: <  xf:output   ref  ="cost"/> </  td  ></  tr  > </  xf:repeat  ></  table  > 

However, doing so would produce an invalid XHTML document; this is because as defined, the content model of XHTML element table cannot be extended. To cover this common use case while preserving the ability to create valid XHTML documents, XForms defines an alternative means of creating repeat structures via a set of attributes. We refer to this set of attributes as repeat attributes .

The repeat attributes encode the same set of information as encapsulated by element repeat , and repeating structures created via repeat attributes have the same functionality as repeating structures created via element repeat . Thus, the difference is one of concrete syntax, and there is no change to the underlying processing model.

To understand this alternative syntax for creating repeating structures, think of repeat as encapsulating its contents in an anonymous group ”anonymous because in the case of element repeat , this group element does not appear in the markup as seen in Figure 4.12.

Viewed this way, element repeat is present in the markup to carry the various information items needed to create the repeating structure; it achieves this by encoding the needed information via attributes appearing on element repeat . Next, we make the anonymous group explicit as shown in Figure 4.15. The repeating structures thus created are identical by our definition.

Figure 4.15 Element repeat contains an anonymous group element.
 <  repeat   xmlns  ="http://www.w3.org/2002/xforms"  id  ="cartUI"  model  ="cart"  nodeset  ="/cart/line-item"  startindex  ="1"  number  ="3">  <!--Anonymous group made explicit -->  <  group  > <  select1   ref  ="item"  appearance  ="minimal"> <  label  >Select Product</  label  > <  itemset   nodeset  ="instance('cat')/product"> <  label  > Description: <  output   ref  ="description"/> Price: <  output   ref  ="price"/> </  label  > <  copy   ref  ="."/> </  itemset  ></  select1  > <  input   ref  ="quantity"><  label  >...</  label  ></  input  > Price + Shipping: <  output   ref  ="cost"/> </  group  ></  repeat  > 

Finally, we can move the attributes appearing on element repeat to element group before dropping the containing repeat element as shown in Figure 4.16.

Figure 4.16 Moving repeat attributes to the containing anonymous group.
 <  group   xmlns  ="http://www.w3.org/2002/xforms"  repeat-id  ="cartUI"  repeat-model  ="cart"  repeat-nodeset  ="/cart/line-item"  repeat-startindex  ="1"  repeat-number  ="3">  <!-- cart UI as before -->  </  group  > 

There is one final twist; we prefix each attribute name in the repeat attributes with repeat- for clarity. See Table 4.1 for the correspondence between attributes of element repeat and the repeat attributes .

Table 4.1. Correspondence between Repeat Attributes and Attributes of Element repeat

Attribute

Repeat Attribute

id

repeat-id

model

repeat-model

nodeset

repeat-nodeset

bind

repeat-bind

startindex

repeat-startindex

number

repeat-number

The XForms working group defined repeating structures via an element (rather than via attributes) to ensure future extensibility. This is why the element version of repeating structures is still the preferred solution over the alternative of placing repeat attributes on element group . However, XForms is also designed to be hosted in a variety of markup languages such as XHTML and SVG. As illustrated in the case of XHTML tables, these container languages may often define content models that are not extensible, thereby making it impossible to use element repeat at the appropriate point in the markup.

XHTML modularization enables host languages to import attributes and apply such foreign attributes to the various elements defined in the language. Thus, the repeat attributes from the XForms namespace can be imported into XHTML and applied to element tr when creating repeating rows inside a table; see Figure 4.17.

Figure 4.17 XHTML tables that grow and shrink dynamically.
 <  table   xmlns  ="http://www.w3.org/1999/xhtml"  xmlns:xf  ="http://www.w3.org/2002/xforms">  <!--Repeat via attributes on element tr-->  <  tr   repeat-id  ="cartUI"  repeat-model  ="cart"  repeat-nodeset  ="/cart/line-item"  repeat-startindex  ="1"  repeat-number  ="3"> <  td  > <  xf:select1   ref  ="item"  appearance  ="minimal"> <  xf:label  >Select Product</  xf:label  > <  xf:itemset   nodeset  ="instance('cat')/product"> <  xf:label  > Item: <  xf:output   ref  ="description"/> Price: <  xf:output   ref  ="price"/> </  xf:label  > <  xf:copy   ref  ="."/> </  xf:itemset  ></  xf:select1  ></  td  > <  td  > <  xf:input   ref  ="quantity"> <  xf:label  >Quantity</  xf:label  ></  xf:input  ></  td  > <  td  > Price + Shipping: <  xf:output   ref  ="cost"/> </  td  ></  tr  ></  table  > 

To summarize, repeating structures can now be encoded by placing repeat attributes as defined earlier on any markup element and consequently on element tr appearing inside XHTML tables. This allows the creation of XHTML tables that grow or shrink dynamically while preserving the structural validity of XHTML documents.

4.3.7 Summary of Construct repeat

We conclude this section with a summary of construct repeat . Element repeat is similar in functionality to iterator constructs found in modern object-oriented languages like Java. Construct repeat enables the creation of browser-based user interfaces for editing and manipulating XML structures.

The children of element repeat define a user interface template that is instantiated for the members of the collection over which the repeat iterates. The repeat index determines the current member of this collection and is key to the functioning of the repeating construct. Access to this repeat index is carefully controlled, and XForms action setindex is the only means of changing the value of the repeat index .

Special XForms actions insert , delete , and setindex can be used to manipulate the underlying collection. XForms user interface controls can be used to view and update individual members of the collection. All such changes to the underlying collection are reflected in the user interface, and this creates dynamic user interfaces.

This design enables the nesting of repeating structures to create complex user interfaces. We detail one such use in Section 4.4 where we use nested, repeating structures to maintain a hierarchical task list. The preferred means of encoding repeating structures is via element repeat . In addition, XForms 1.0 defines a set of repeat attributes that provide the same functionality as element repeat for use in container languages where the content model does not permit new elements.



XForms. XML Powered Web Forms with CD
XForms. XML Powered Web Forms with CD
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 94

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net