Emerald Hand Wiki

Back to tutorials section


Edit

Introduction

This tutorial will show you how to create abstract types. An abstract type is usually a data structure, such as list, graph or tree, designed to be extended to organize other types of data. The type provides common generic operations to work with the structure independent of the data stored in it.

Abstract types are usually used by embedding other types in it. For example, an abstract tree + HTML will result in a tree of notes, abstract list + single movie is a collection of movies, and so on.

Edit

Analysis

These are the reasons we are considering creating this type:
  1. To learn working with abstract types.
  2. Laying out foundation. Later on we will use this type to add support for more complicated documents.
  3. Provide foundation for the user types to store tabular data. First this type might be used to import data from a database, but more likely it will be used to create new Sider unique types, such different collections.

And here are some things the type should support:

  1. Abstract items list. It should support extension to allow any specific data type inside each list item.

  2. Simple way to identify and distinguish items in the list. View might show the list, sort it, delete items in the middle of the list and all items should still be easy to access. We could use item position as item ID, but this would cause problems with deletion of an item in the middle of the list. All consecutive items will have a new position, but their view id (which was originally generated based on their position) will be off.

    The best idea I can come up with is to use incremental counter. Every time a new item is added the counter is used to generate new item id and increments by one. There are two caveats.


    1. We need to store the counter somewhere. I see no better place to do that than the items parent element.

    2. ID will need to be unique in the whole document. If there's another list each list ids need to be different. Also, item valid id can have alphanumeric characters, but must start with a letter. In light of this we are going to generate id for the items parent when document is created and use it as a prefix for list item ids.



Edit

Prototype

Here's an example of what I'm talking about.

<document>
<list id="lst101" idcounter="5">
<item id="lst101_1"><name>Item 1</name><content/></item>
<item id="lst101_2"><name>Item 2</name><content/></item>
<item id="lst101_3"><name>Item 3</name><content/></item>
<item id="lst101_4"><name>Item 4</name><content/></item>
</list>
</document>

When a new item is added to the list its id will be lst101_5 and idcounter will increment to 6.

Edit

Descriptor

As with all types you start be creating type.xml.
  • id - Generate new GUID for the type
  • name - List
  • version - 0.5.0.0
  • category - abstract
  • prefix - list

Edit

Schema

The schema isn't very complicated.

<xs:redefine schemaLocation="sider">
<xs:complexType name="DocumentContentType">
<xs:complexContent>
<xs:extension base="list:DocumentContentType">
<xs:sequence>
<xs:element ref="list:list"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:redefine>

<xs:element name="list">
<xs:complexType>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:attribute name="idcounter" type="xs:unsignedLong" use="required"/>
<xs:sequence>
<xs:element ref="list:list" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>

<xs:element name="item">
<xs:complexType>
<xs:attribute name="id" type="xs:ID" use="required"/>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element ref="list:content" />
</xs:sequence>
</xs:complexType>
</xs:element>

<xs:element name="content" abstract="true"/>

We declare the document and a list element for all items and items. Each item has an ID and content elements. Child types can substitute content element with something new (that could embed other documents).

Edit

Constructor

Most complicated part of the constructor is unique id generation. We will rely on XSLT {generate-id} method for that. List constructor looks something like this:

<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates />
<list:list id="{generate-id(.)}" idcounter="3">
<list:item id="{generate-id(.)}_0">
<list:name>Item 1</list:name>
<list:content />
</list:item>
<list:item id="{generate-id(.)}_1">
<list:name>Item 2</list:name>
<list:content />
</list:item>
<list:item id="{generate-id(.)}_2">
<list:name>Item 3</list:name>
<list:content />
</list:item>
</list:list>
</xsl:copy>
</xsl:template>

Edit

Script

Adding code is more complicated. We will begin by declaring methods to access list items. Later we will add methods to add and delete them.

Edit

Item access

First method is getItemPath. It will generate item path using supplied id. Because we rely on id this is going to be extremely simple.


/// Region Constructor
/*
Constructor: initializer
Initializes a new instance of the object.
*/
initializer: function()
{
/*
Variable: name.list
Qualified list name.
*/
this.name.list = this.formQName( "list" );
/*
Variable: name.item
Qualified item name.
*/
this.name.item = this.formQName( "item" );
/*
Variable: name.name
Qualified name element.
*/
this.name.name = this.formQName( "name" );
},
/// End
/*
Function: getItemPath
Returns XPath to the item specified by id.

Remarks:
Assumes *id* is unique for the whole document.
*/
getItemPath: function( id )
{
return "//" + this.name.item + "[@id='" + id +"]";
}

All we do is reference document by id, no matter where it would be at. This approach assumes id is unique for the whole document.

Edit

get/setName

Next we are add methods to access item content. We start with the name.

/*
Function: getName
Returns item name.
*/
getName: function( xpath )
{
return this.getValue( xpath, "name", false );
},
/*
Function: setName
Sets item name.
*/
setName: function( xpath, value )
{
this.setValue( xpath, "name", value, false );
}

Edit

getContent

We are going to try and add a method to access the item content, but there's a small issue. Content element can be substituted (substitutionGroup) in other types inherited from the list. We can't rely on its name to form XPath. Instead we will rely on its position (it's either the last item or name sibling. We'll use next sibling approach).


/*
Function: getContentPath
Returns path to the item content element.
*/
getContentPath: function( xpath )
{
var namePath = sider.xml.xpath.concat( xpath, this.getFieldPath( "name" ));
return sider.xml.xapth.concat( namePath, "following-sibling::*[0]" );
}

Edit

Delete item

Adding a method to delete an item is quite straight forward. sider.xml package provides everything we need. We well require author to provide a valid XPath to the item to delete. In most cases they can use the method we supplied for that.

The reason I don't want to accept id by the deleteItem method because I want to force view author to be aware of the type reliance on valid XPath. Sure, in most cases it will be possible to generate path using getItemPath method, but not always. Sometimes the view developer might use other mechanisms to reference an item.

We could also try and accept both XPath and id, but we would have to accurately distinguish between them, since both are strings. I'd rather avoid making possible mistakes, keep code simpler and force view developer to keep in mind how things work.


/*
Function: deleteItem
Deletes an item pointed to by XPath.
*/
deleteItem: function( xpath )
{
sider.xml.deleteElement( xpath );
}

As you notice this method will delete any XML element if XPath is valid. Later, if we desire, we will be able to add validation code to ensure XPath points to a list item. For now, to keep things simple, we'll just delete anything XPath points to.

Edit

Add item

This is the last method we need. It will create a new item and return its id. Most likely it will be used by the types inherited from the list. The new item will have no content and it will have to be added outside of this method. At some point we might add events or event topics (Dojo global events) to allow other types add content automatically when new item is added. But we don't need that feature right now and I'm not sure of the best way to handle this. Later, when we are more aware of how the list is used, we will come back to this.

Remember that we need to generate an id for the new item and increment idcounter. We are going to create some misc methods to help us with that.

/*
Function: getNewItemId
Returns a new unique id for the item.
*/
getNewItemId: function( docPath )
{
var counter = this.getIdCounter( docPath );
this.incrementIdCounter( docPath );
return this.generateItemId( docPath, counter );
},
/*
Function: getIdCounter
Returns ID counter.
*/
getIdCounter: function( docPath )
{
return this.getValue( docPath, "list/@idcounter" );
},
/*
Function: incrementIdCounter
Increments id counter by 1.
*/
incrementIdCounter: function( docPath )
{
var counter = this.getIdCounter( docPath );
counter = parseInt( counter );
counter += 1;
this.setValue( docPath, "list/@idcounter", counter );
},
/*
Function: getListId
Returns list id by the list path.
*/
getListId: function( docPath )
{
return this.getValue( docPath, "list/@id" );
},
/*
Function: generateItemId
Generates id for the specified item.
*/
generateItemId: function( docPath, itemId )
{
return this.getListId( docPath ) + "_" + itemId;
}

And the method to add new items:

/*
Function: addItem
Adds a new item to the list.

Returns:
New item ID.
*/
addItem: function( docPath, itemName )
{
var listPath = this.getPath( docPath, "list" );
var newItemPath = this.addElement( listPath, "item" );

var itemId = this.getNewItemId( docPath );
sider.xml.setAttribute( newItemPath, "id", itemId );

var itemNamePath = this.addElement( newItemPath, "name" );
sider.xml.setText( itemNamePath, itemName );

return itemId;
}

All this method does is create item structure, node by node. It relies on the methods we have supplied earlier to get new item id.

Edit

Conclusion

The abstract list type is done. There are some parts that are unclear that we might come back later to, such as validation of the item to delete and adding content to a new item.

The main problem is that there're no testing facilities in place to verify type works correctly. You will need to create a type derived from this and a view to verify all code functions correctly.

Edit

Versioning

As you're using the type you might find that some features are missing. You could rush back and create a new type, but Sider supports extensions versioning. If you are going to add new features you should create new type version and use it instead (remember to keep type GUID the same).

Referencing different type versions is easy enough, and by default, when you don't supply the type version the latest one is used.

Copyright © 2006-2008 Emerald Hand, Inc. All rights reserved.
Powered by ScrewTurn Wiki version 2.0.34. Some of the icons created by FamFamFam.