Neil's Place

August 17, 2005

8:32 PM How Templates Work XXXVII - RDF Modifications

The third type of observer involved in a template builder is an nsIRDFObserver. The template builder implements this interface to listen for RDF modifications. When the datasource is modified, the datasource will notify any observers of the change. The template builder uses these notifcations to update the template as necessary based on the new or removed information. You don't need to implement this observer yourself, although you may add an observer to the datasource if you want to be notified when the data changes.

There are two main situations when the notifications are made. The first is when the modification functions on the datasource are called. There are four such functions: 'Assert', to add a new triple (or arrow) to the RDF graph, 'Unassert' to remove a triple, 'Change' to adjust the target of a triple, and 'Move' to adjust the source of a 'triple'. For Mozilla's datasources, the latter two just Unassert the old triple and add a new one, creating the effect of changing the value. However, only one notification is made.

For instance, an Assert call looks like the following:

var source = RDF.GetResource("http://www.xulplanet.com/ndeakin/images/t/obelisk.jpg");
var predicate = RDF.GetResource("http://purl.org/dc/elements/1.1/description");
var target = RDF.GetLiteral("One of the thirty or so Egyptian obelisks");
datasource.Assert(source, predicate, target, true);

The Assert call adds a new triple to the RDF datasource. When this happens, any templates observing the datasource will be notified via the RDF observer's onAssert method.

The second situation when notifications are made is when a datasource is being loaded or reloaded. Actually, internally, this isn't any different than the other notifications, but it is worth discussing separately. When the RDF parser loads RDF/XML, it starts with a new empty datasource, and as the parser parses the input data, it calls the datasource's Assert function to add each found triple. In effect, this isn't any different than adding the same set of triples yourself using the Assert method.

When reloading a datasource, you might think that the RDF parser removes all the existing data, loads the new data, and adds it to the datasource. Or, you might think that it creates a fresh datasource with the new data. Actually, the parser does something smarter. When reloading a datasource, it keeps the existing RDF triples intact, and only modifies the datasource based on what has changed. When parsing, any triples that already exist are not added again. If a triple does not exist yet, it will be added. Any triples that don't exist in the new data but were there before are removed. This means that the observer will be called only for the triples that differ between the new and old version of the data. If the reloaded datasource hasn't changed, the builder won't receive any notifications. This saves a lot of extra work.

The RDF observer also has two methods onBeginUpdateBatch and onEndUpdateBatch. These are called when performing a lot of operations on a datasource. When changing the datasource, the changes are surrounded by begin and end batch calls. Then, rather than notify on every change, the datasource will send one notification when the changes are finished. The template builder then rebuilds the template completely when done. This is useful when making a large amount of changes to avoid having to keep recalculating parts of the template that might change again quickly.

We'll look at some specifics of how the template builder handles changes next.

Comments ( 0 )

August 15, 2005

7:46 PM How Templates Work XXXVI - Tree Builder Listeners

The second type of listener is used to handle particular actions related to trees. The tree builder implements the nsITreeView interface, so handles the gathering of data and passing it on to the tree. The tree widget informs the view when certain operations are performed that might affect the data. The tree view handles all of these operations, but allows an observer to be attached which is invoked during these operations. For instance, the observer may have an onToggleOpenState method which will be called when the user opens or closes a row. The tree builder will handle the adding or removing of rows, but will call the observer so that it can perform some task.

The tree builder observer implements the nsIXULTreeBuilderObserver interface and may be attached to a tree builder using the builder's addObserver method. You can add more than observer if needed, and can remove them again with the builder's removeObserver method.

The observer is always invoked before the appropriate operation is performed. For instance, the onToggleOpenState method of any observers will be called before the tree item is opened. After the observers have finished, the tree builder opens the row and adds any child rows inside. Note that you cannot cancel the operation from within the observer.

Some useful functions of the observer are the drag and drop related callbacks to handle when an item is dragged onto the tree. This makes handling dragging onto a tree fairly simple. All you need to do is implement two methods, canDrop and onDrop. Note that in Firefox 1.0 (Mozilla 1.7) and earlier, the drag functions are slightly different. There, three functions are used, canDropOn, canDropBeforeAfter and onDrop. The two 'can' functions were combined into one with an extra argument. If you want to support both earlier and newer releases, you can implement all of the functions in the observer, sharing code as necessary.

The tree observer receives drag related events in three places: over a container row, before a row, and after a row. This allows you to handle dragging with more flexibility. For example, in some situations you may want to require dragging onto a folder type of row. In other situations, you may wish to allow items to be dragged between (before or after) rows. This would be the situation if you were dragging items from that tree around, for instance dragging a bookmark from one location to another. The tree widget will draw a small line between the rows while dragging. All you need to do is add a tree builder observer which returns true for the canDrop method. Note that the 'drag on' case only allows dragging onto containers, not ordinary rows.

var treeBuilderObserver = {
  canDropBeforeAfter : function(idx, orient) { return false; },
  canDropOn : function(idx, orient) { return true; },
  canDrop : function(idx, orient) { return !orient; },
  onDrop : function(idx, orient) {
    // do something here
  },
};
tree.builder.addObserver(treeBuilderObserver);

This observer implements both the older and newer methods and only allows dragging on rows. The canDropBeforeAfter method returns false since we do not want to allow before and after drops. The canDropOn method returns true however. The Mozilla 1.8 method canDrop checks the orientation and returns the opposite. This works as the 'on' value is 0 and the 'between' values are -1 and 1. Obviously, this code is much simpler than what we would really want to use -- we should be checking what is being dragged to make sure that it is compatible with the tree. Or, we might want to allow dropping on specific rows only; the drop methods are supplied with an index argument so we can check for this.

Comments ( 1 )

August 14, 2005

12:07 AM How Templates Work XXXV - Template Listeners

There are several listeners (or observers) used during the template build process, each used for different purposes. These each implement a different XPCOM interface, as listed below:

The first of these is the simplest and involves two methods, willRebuild and didRebuild. You would implement this object with these two methods if you wish to be notified when the template is rebuilt using the builder's rebuild call. The template builder might also force a rebuild when the underlying data change notifications require it. The primary use of this listener is to store some state before the template is rebuilt and restore it afterwards. Recall that when a template is rebuilt, all of the existing content will be removed and generated fresh. The willRebuild method of any listeners will be called before the content is removed, and didRebuild method will be called when the content has been regenerated. This listener will also work for tree builders, and will call the appropriate methods before and after the tree has been generated.

To assign a builder listener to a builder, use the addListener method.

var someListener = {
  item: null,
  willRebuild : function(builder) {
    this.item = builder.getResourceAtIndex(builder.root.currentIndex);
  },
  didRebuild : function(builder) {
    if (this.item) {
      var idx = builder.getIndexOfResource(this.item)
      if (idx != -1) builder.root.view.selection.select(idx);
    }
  }
};
tree.builder.addListener(someListener);

This example is very simple and just saves and restores the selected index after a rebuild. Since the content goes away during a rebuild, the selection is lost, so it is restored here during the didRebuild method. The Firefox bookmarks window uses this technique. If you try an example using the code above, you will notice that the first tree will maintain the selection when the Rebuild button is pressed, whereas in the second tree does not. This is because the listener is only attached to the first tree.

The example above makes use of the getResourceAtIndex and getIndexOfResource methods. These two methods are available for tree builders and will convert between an index in the tree and the associated member resource for the item at the index. Naturally, we can't store the index as the item may have moved its position. Or, the resource may no longer be part of the results, which is why we need to check the return value of the getIndexOfResource method. (As this example uses the RDF resources directly, it requires elevated privileges so you will need a chrome URL to test it.)

You might also guess that the builder's root property, which is used above, refers to the tree. In a content builder, it will return the element with the datasources attribute, which in the template builder is referred to as the root element.

Finally, you can remove a listener using the builder's removeListener method.

Comments ( 0 )

August 9, 2005

8:00 PM How Templates Work XXXIV - Rebuilding Templates

The datasource associated with the template can be retrieved using the element's "database" property. It implements the nsIRDFCompositeDataSource interface. Since this is a composite datasource, it may contain more than one datasource. These may be listed in the datasources attribute separated by spaces. For example:

<vbox datasources="template-guide-photos5.rdf template-guide-streets.rdf">

Sometimes, you will want to calculate the datasource to be used and attach it to the template later. You can do this using the composite datasource's AddDataSource method. You can add as many datasources as you wish, and you can remove them using the RemoveDataSource method. A common pattern is to use the following:

var RDF = Components.classes["@mozilla.org/rdf/rdf-service;1"].
            getService(Components.interfaces.nsIRDFService);
var ds = RDF.GetDataSource("http://www.xulplanet.com/ndeakin/tests/xul/template-guide-streets.rdf");
var tree = document.getElementById("theTree");
tree.database.AddDataSource(ds);
tree.builder.rebuild();

This is the typical way to add a datasource to an element, in this case to the tree with the id "theTree". The datasource is retrieved using the RDF service's GetDataSource method. After adding a datasource, the tree builder's rebuild method is invoked to rebuild the template with the new data. This doesn't happen automatically when you add the datasource, which is useful, since you will often want to add or remove other datasources at the same time.

The composite datasource can only be accessed from privileged code, regardless of what datasources it contains. Fortunately in this situation, you can just set the datasources attribute (or the corresponding property) to the datasources that you want. For instance:

var tree = document.getElementById("theTree");
tree.datasources = "template-guide-photos5.rdf template-guide-streets.rdf";

This will also change the datasources used. In this case, the template will be rebuilt automatically as you can set all of the datasources at once with this method of changing datasources. So you don't need to call the rebuild method. You can also do the same with the ref attribute (or the ref property) and the template will be rebuilt automatically. In the example above, there will be two datasources attached to the tree. They will both be loaded and the template reconstructed. Note that if one of the datasources is already loaded it will not be loaded again. This is convenient when you want to simply add a new datasource to an existing template without reloading existing data. If you do want to reload the data, you can call the builder's refresh method:

tree.builder.refresh();

This will reload the datasource attached to the template. If there is more than one datasource, as above, all of them will be reloaded. Due to the nature of the way templates are updated, you don't usually need to rebuild a template after a refresh call, although there may situtations where this will be necessary.

If you do plan on determining the datasources dynamically, it is common to start with an empty datasource using the special URI "rdf:null".

<tree datasources="rdf:null" ref="http://www.xulplanet.com/rdf/myphotos">

This will create a composite datasource with no datasources in it. This syntax is necessary as otherwise you wouldn't be able to specify a value for the datasources attribute, and a template builder would not be attached to the element. In a chrome context, the datasource rdf:local-store is always included even if you don't specify it. This is something to watch out for if you are going to be manipulating the composite datasource.

One more note: the datasources attribute may use either absolute or relative URLs. Relative URLs are relative to the XUL document the corresponding element is in. The RDF service's GetDataSource method however, only accepts absolute URLs. So you will need to use the full path in this situation.

Comments ( 0 )

August 6, 2005

10:48 AM How Templates Work XXXIII - Attaching the Template Builder

When inserting an element into a XUL document, the element is checked to see if it has a datasources attribute. If so, a template builder will be created for the element and attached to the element. If the element is a <tree> element and has the flags attribute set to "dont-build-content", a tree builder will be created. Otherwise, a content builder will be created. Both types of builder share much of the same code except for how they generate output to be displayed. Both types of builders implement the nsIXULTemplateBuilder interface, while the tree builder also implements the nsIXULTreeBuilder interface.

The builder associated with an element is accessible via the element's "builder" property both for content builders and for tree builders. An element that does not have a builder will have this property set to null. The processes of creating a builder for an element applies both when an element is created when the window is loaded and when an element is inserted dynamically.

Templates can only be used in XUL documents, however, there is no requirement that the templates generate XUL elements. They could also be used, for example, to generate HTML elements. This isn't a very common technique, however, here is an example of how this can be used:

<html:div id="photosList" datasources="template-guide-photos5.rdf"
          ref="http://www.xulplanet.com/rdf/myphotos"
          xmlns:html="http://www.w3.org/1999/xhtml">
  <html:h1>My Photos</html:h1>
  <template>
    <html:p uri="rdf:*"><textnode value="rdf:http://purl.org/dc/elements/1.1/title"/></html:p>
  </template>
</html:div>

This example generates three paragraphs. Some static content before the <template> element displays a <h1> header. Since templates were designed for creating XUL content, sometimes there can be unusual results when using HTML. Sometimes this is due to different whitespace handling for HTML and XUL, which is why the content to generate in the above example is all on one line. If you do plan on generating non-XUL content with a template, just watch out for issues like this. Note that this particular whitespace issue has been fixed in later Mozilla builds.

Note also that the datasources attribute has been placed on a non-HTML element. This is also allowed. However, one thing to watch out for is that non-XUL elements do not have their content generated lazily so all of the content will be generated at once. Be careful that your templates do not recurse to deep levels.

The builder property is a property of the nsIDOMXULElement interface, so all XUL elements will have this property, although, as mentioned earlier, is will be set to null for most elements. For non-XUL elements, the template builder will be assigned to a builder property on the element using a custom JavaScript property instead.

The main purpose of accessing the builder for an element is to call its "rebuild" method. This method removes any existing generated content and deletes all data in the rule network. Then, the method recompiles the rules and regenerates the content. Essentially, the rebuild method instructs the builder to remove any existing information and reconstruct it from the beginning. The only difference is that the datasource has already loaded so the data will be the same. However, this is often used if you modify the datasource or modify the rules.

The builder's refresh method, however, will reload the datasources. It will not rebuild the template, but we'll see in a later section why this is not usually necessary. To summarize, the refresh method reloads the data, whereas the rebuild method reconstructs the content.

The builder is accessible to unprivileged code, so the rebuild and refresh methods may be called by remote code.

We'll see some examples of rebuilding templates next.

Comments ( 2 )

August 1, 2005

9:09 PM How Templates Work XXXII - Hierarchical Trees

A template may be used to generate hierarchical trees. This works just like with recursive generation using the content builder. Each level of the tree is created using a successive iteration of the template build process. If the items are containers, the tree builder will mark the right rows as containers, so that they can be opened and closed with the small icon twisties on the left of the column. Remember to make the left column the primary column for these to appear.

To be able to do this, the tree builder must know that an item is a container. Usually you would display a tree from data in an RDF container such as a Seq. In this case, determining that the node is a container is easy. If a node is a RDF container, the tree item becomes a container, and the user may open the row by double-clicking it. Note that this test is done on the member value not the reference value. For instance, in the photo example, we have a container "http://www.xulplanet.com/rdf/myphotos" with three photos. Three results will be generated from a simple rule with no extra conditions. It is the result, or the photo, that will be checked, not the container of photos. Since a photo isn't a container, the tree rows will not become containers, so you will not be able to open them. As the rows are not containers, the tree builder does not recurse to find additional data. The tree builder creates rows lazily, so a closed container will not have any data generated inside in it until the row is opened. When the user opens the tree row, the next level of rows are generated from the template and displayed in the tree. Similarly, when the user closes a tree row, the rows inside it are removed, such that they will have to be generated again the next time the row is opened.

If you want to put rows inside the photo rows, you will either need to make each photo resource an RDF container, or use the containment attribute to specify additional properties that indicate containership. If a particular photo had a value for one of the properties listed in the containment attribute, it would be accepted as a container, and the user could open the row. When the user opens the row, the template will be re-examined for results using the photo as the starting point instead of the top level ref value.

Here is an example for the streets datasource:

<tree id="photosList" flex="1" datasources="template-guide-streets.rdf"
      ref="http://www.xulplanet.com/rdf/myneighbourhood" flags="dont-build-content"
      xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <treecols>
    <treecol id="address" primary="true" label="Address" flex="1"/>
    <treecol id="floors" label="Floors" flex="1"/>
  </treecols>
  <template>
    <rule rdf:type="http://www.xulplanet.com/rdf/House">
      <treechildren>
        <treeitem uri="rdf:*">
          <treerow>
            <treecell label="rdf:http://www.xulplanet.com/rdf/address"/>
            <treecell label="rdf:http://www.xulplanet.com/rdf/floors"/>
          </treerow>
        </treeitem>
      </treechildren>
    </rule>
    <rule>
      <treechildren>
        <treeitem uri="rdf:*">
          <treerow>
            <treecell label="rdf:http://purl.org/dc/elements/1.1/title"/>
          </treerow>
        </treeitem>
      </treechildren>
    </rule>
  </template>
</tree>

This is similar to a previous example except that it uses a tree. The first rule is for the houses as indicated by the rule's condition and the second rule is for the streets. As shown in the snippet of the data below, the street is a Seq, so it will become a container. The houses are not containers, so they will not have children in the tree.

<rdf:Bag rdf:about="http://www.xulplanet.com/rdf/myneighbourhood">
  <rdf:li>
    <rdf:Seq rdf:about="http://www.xulplanet.com/rdf/marion"
               dc:title="Marion Street">

The result is a two level tree with two columns.

Comments ( 0 )

July 31, 2005

8:23 PM How Templates Work XXXI - Tree Builder Features

Besides the label of a cell, there are several other cell properties you can set when using the tree builder. The supported properties are: label, mode, properties, src and value. The label attribute is used to set the label for a cell. The mode is used for progress meter columns. It may be set to either 'normal' for a normal progress meter or 'undetermined' for an undetermined progress meter. The value attribute is used to set the current progress value for normal progress meters. The value attribute may also be used for checkbox columns by setting it to either true or false. Whether a cell is a normal labeled value, a progress meter or a checkbox is determined by the type attribute on the column the cell is in.

For cells in normal columns, you can use the value attribute to store some other value and you can use the view's getCellValue method to retrieve it. Naturally, this will retrieve the value after any variables have been substituted. Besides the attributes mentioned above, any other attributes specified on the tree rows and cells are ignored. Since no elements are generated, you won't be able to retrieve the values for them either. Thus, the value attribute may be useful to associate an additional value with a row since it will be easier to retrieve.

The src attribute may be used to set an image to appear in a cell. For example:

<tree id="photosList" flex="1" datasources="template-guide-photos5.rdf"
      ref="http://www.xulplanet.com/rdf/myphotos" flags="dont-build-content">
  <treecols>
    <treecol id="photo" label="Photo" flex="1"/>
  </treecols>
  <template>
    <treechildren>
      <treeitem uri="rdf:*">
        <treerow>
          <treecell src="rdf:*"/>
        </treerow>
      </treeitem>
    </treechildren>
  </template>
</tree>

This tree displays each photo in the tree cells. In this case, the member resource is used since that holds the photo's URL, however it could be any other variable, a static value, or a combination of both.

Of course, we can't really see the photos, since the tree's rows are too small. Normally, you wouldn't put photos in a tree like this; instead the images would be used for icons. However, you could use a stylesheet to change the default height of the tree rows. You cannot make each row a different height, but you can change the height of all rows with some CSS:

treechildren::-moz-tree-row {
  height: 150px;
}

Since no elements are constructed by the tree builder, you cannot use the style or class attributes to change the style of a cell (This is the case with all trees). You must use syntax like that above to change the appearance. In the example above, it changes the height of a row to 150 pixels. You may want to change the syntax to refer to a specific <treechildren> element rather than all of them. Once the row height is changed, we can see the entirety of the photos.

Since we need to use special CSS for trees, the properties attribute on a cell becomes useful. It can be used to define extra properties that can be refered to in a stylesheet. For example, if the properties attribute was set to the value "?creator", you could style the photos created by different people differently. You can also use static values in addition to variables in the properties attribute. For instance, consider the following CSS:

treechildren::-moz-tree-cell(Dave) {
  background-color: lightgreen;
}

This would set the background colour of a cell to green for any cell with the "Dave" property. You can also use the properties attribute on the <treerow> to change the style for an entire row. This example sets the country associated with a photo as a property of a tree's rows. We can use that property to change the appearance of each row.

<rule>
  <conditions>
    <content uri="?start"/>
    <member container="?start" child="?photo"/>
    <triple subject="?photo"
            predicate="http://www.xulplanet.com/rdf/country"
            object="?country"/>
    <triple subject="?country"
            predicate="http://purl.org/dc/elements/1.1/title"
            object="?countrytitle"/>
  </conditions>
  <action>
    <treechildren>
      <treeitem uri="?photo">
        <treerow properties="?countrytitle">
          <treecell src="?photo" label="Cat"/>
        </treerow>
      </treeitem>
    </treechildren>
  </action>
</rule>

You might use the following CSS to change the border around rows with a particular country:

treechildren::-moz-tree-row(Netherlands) {
  border: green 1px solid;
}

The result of this example is a tree where one row has a green border around it.

Comments ( 0 )

July 27, 2005

7:21 PM How Templates Work XXX - Building Trees

The most common element to use with a template is the tree. You can use a template with a tree just like any other template. However, since templates are often used with trees, especially with large amounts of data, the template system supports a special builder just for creating trees. Rather than generate content for each row in the tree, the results are just stored in a list inside the builder. This means that DOM nodes are not constructed for any of the items. This is much more efficient as creating a lot of DOM nodes would add a lot of additional overhead. This performance advantage is possible since trees can only display text so the builder only has a few pieces of information to keep track of.

To use the tree builder, you need to add a flags attribute to the root node:

<tree datasources="template-guide-streets.rdf"
      ref="http://www.xulplanet.com/rdf/myneighbourhood"
      flags="dont-build-content">

The "dont-build-content" flag is descriptive in that it doesn't cause any content to be built. However, what it really does is use a subtype of the main builder specific to trees, called the tree builder. Without this flag, the template will be handled using the other type of builder, which is called a content builder, as it generates content. Note that while a tree builder can only be used with trees, a content builder can be used with any type of content. You can also choose to use the content builder for a tree, if you wish. There may be uses for this, especially for small amounts of data. However, you will find that the content builder will be slower as the amount of data to display gets larger.

Apart from the flags attribute, the template syntax is exactly the same for the tree builder as with the content builder. One thing though is that the tree builder requires a very specific form to the action body, specifically, the action body should be a single treeitem with its row and cells. Here is an example:

<tree id="photosList" flex="1" datasources="template-guide-photos5.rdf"
      ref="http://www.xulplanet.com/rdf/myphotos" flags="dont-build-content">
  <treecols>
    <treecol id="name" label="Name" flex="1"/>
    <treecol id="date" label="Date" flex="1"/>
  </treecols>
  <template>
    <treechildren>
      <treeitem uri="rdf:*">
        <treerow>
          <treecell label="rdf:http://purl.org/dc/elements/1.1/title"/>
          <treecell label="rdf:http://purl.org/dc/elements/1.1/date"/>
        </treerow>
      </treeitem>
    </treechildren>
  </template>
</tree>

The tree columns are declared as static content since we only want to declare them once. This template uses the simple rule syntax, although the extended syntax could also be used. The uri attribute must be declared on the <treeitem> element set to either "rdf:*" for the simple syntax or the member variable for the extended syntax. The remaining tags are like the syntax of a tree with a single row. This row will be used as the template data by the tree builder. Instead of generating content, the builder will use the cell attributes to determine what to display. The tree builder implements the nsITreeView interface so it becomes the tree's view. (That is, the tree's view and the tree's builder are the same object.) When the tree is displayed, it asks the view for the contents of each cell. The builder looks at the label for the corresponding cell, translates any variables or predicates into values, and returns the value.

In the example above, the first cell should display the title. The builder doesn't compute any labels until the view asks for them. When the view does request a label for the first cell, the builder looks up the "http://purl.org/dc/elements/1.1/title" predicate for the row in question and returns it.

The content builder will generate the content in the template body and do substitution of the RDF predicates right away. However, it will generate the same result on screen to the user as with the tree builder. Compare the example with a tree builder and the same example using a content builder.

Comments ( 0 )