ADP Master Pages Tcl Module


"Master pages allow you to create a consistent layout for the pages in your application. A single master page defines the look and feel and standard behavior that you want for all of the pages (or a group of pages) in your application. You can then create individual content pages that contain the content you want to display. When users request the content pages, they merge with the master page to produce output that combines the layout of the master page with the content from the content page." - MSDN ASP.NET 2.0

Conceptually our ADP Master Pages work the same way as ASP.NET 2.0 Master Pages.  A content page defines one or more content blocks and declares what the master page is and the master page has content placeholders to receive the content from those blocks.  We refer to those content blocks as pouring themselves into those placeholders.

Comparison to ASP.NET 2.0 Master Pages

One key difference between ASP.NET Master Pages and our ADP Master Pages is the sequence in which the code in each page is executed.  In ASP.NET, the content page declares the master page it uses at the very top and the master page is processed first and then the content page (I am simplifying the ASP.NET situation slightly).  In our ADP Master Pages, the content page is processed first and then at the very bottom it essentially ns_adp_includes the master page (only instead of the output of the included master page becoming part of the content page like with regular includes, the content page's content is included into the master page).

This sequence not only vastly simplifies the implementation, but in my opinion, is also more useful and flexible as you'll see below.  (For now I'll mention that code in the content page can alter the destination master page or placeholder at runtime.)  Top-down sequence (i.e. first some general site-wide initialization, then the page) can already be obtained through the use of AOLserver filters.

Another improvement over ASP.NET Master Pages is their content pages are page fragments (i.e. no BODY tag - the master page has the BODY tag).  Our ADP content pages are valid HTML pages that can even contain some kind of mock-up layout markup that is discarded when the content is poured into the master page.  This allows these content pages to be easily edited by writers and designers in any WYSIWYG HTML Editor such as Dreamweaver of FrontPage and during editing they look very similar to the final output.  There is also no requirement to name master page files anything special.

In ASP.NET, content blocks are declared for pouring into content placeholders with a certain id (i.e. "main").  We support the same thing, plus, you can have one unnamed content block / placeholder, which can be convenient.

In addition, it is frequently necessary for the content page to add things to the attributes of tags in the master page (i.e. add a page-specific Javascript function to the onload attribute of the BODY tag).  We provide an easy to use facility to do this.

Finally, in modern AJAX-style web interfaces, it is useful to retrieve just the content of a particular block without pouring it into the rest of the page (i.e. Partial Page Rendering like in an ASP.NET AJAX/ATLAS Update Pane control).  Our ADP content pages will return to the client just one block if so requested via a query string parameter (but be aware that the entire content page still gets executed (the master page does not)).

How does it work?
Our Master Pages implementation is actually very simple and quite efficient.  Essentially it's a wrapper around the ADP buffer manipulation commands - ns_adp_dump and ns_adp_trunc that consists of the following four main steps:
  1. Each content block has a start marker which truncates and discards the adp buffer accumulated to this point.  This is actually optional but is useful to discard mock-up layout markup, html/head/body tags, etc. from the content page.
  2. Each content block has an end marker which stores away the current buffer (which contains the output of the content block) in a global array and also truncates it.
  3. After the closing </html> (so that it can be discarded) of the content page, the master page is included into.
  4. The master page retrieves the previously stored content blocks and places them into appropriate placeholders.
Syntax Forms

There are three different syntax forms for each of the steps above:

  • Calling a Tcl command from inside a <% tag
    (i.e. <% am_adp_content start "main" %>, for example, for the start marker)

  • Using a fancy tag (i.e. <adp:contentstart contentplaceholderid="main"> for the same thing)

  • Using our new ASP.NET-style declarative directive syntax:
    <%@ ContentStart ContentPlaceHolderId="main"%>
    - this syntax form requires our amattributes2set C Module.

Which syntax form you use is mostly a matter of personal preference and you can also mix and match all three forms as you see fit.  We recommend the <%@ declarative syntax within the content page and the fancy tag syntax within the master page for the following reasons:

  • Declarative syntax is not case sensitive

  • Declarative syntax is more understandable than just using the Tcl command and accommodates future changes better (to do the same with the Tcl command requires that it is implemented with named parameters which is a little more work and it's still not as flexible and most importantly will be very foreign to a designer who doesn't know Tcl)

  • Unlike a fancy tag, declarative syntax can still use Tcl variables or commands
    (i.e. <%@ ContentStart ContentPlaceHolderId="$myid"%>)

  • Conceptually, a fancy tag is expected to return something in its place but that's not what happens here - instead we declare markers or what the master page is, so declarative syntax just makes more sense.

  • On the master page, however, it makes sense to use fancy tags, because they don't need variables, they do return content and most importantly, the specific tag used in the master pages requires a closing tag, so that the string inside the tag is what's returned if the content block is undefined, i.e.:
    <adp:contentplaceholder id="main">Default</adp:contentplaceholder>

Note that the fancy tag syntax is the closest to how ASP.NET Master Pages are implemented, EXCEPT, the fancy tag for the content block does not enclose the content like it does in ASP.NET, it only marks the start of it.

So, first the four main steps in each of the syntax forms:

<% am_adp_content start ?contentPlaceHolderId? %>
<adp:contentstart ?contentplaceholderid="contentPlaceHolderId"? />
<%@ ContentStart

Truncates/discards the current adp buffer.
Also optionally sets the intended contentplaceholderid in
the master page for the content block about to start.
Using start is optional and is only needed to discard
redundant html, body, etc. tags that are in the page
just to keep it as legal html.

<% am_adp_content end ?contentPlaceHolderId? %>
<adp:contentend ?contentplaceholderid="contentPlaceHolderId"? />
<%@ ContentEnd

Stores the current adp buffer as a content block,
then truncates the buffer. The supplied id of the content block
(which determines which placeholder it will be placed into)
is used ONLY if the id has not already been set with either
am_adp_content start (or <%@ ContentStart / <adp:contentstart)
or am_adp_content setcurrentid (of if the currently set id is blank).
If no id is ever set in either start, setcurrentid or end,
this becomes the default/unnamed content block (you can only have one).

<% am_adp_directive_includeinto [am_set frommap masterpagefile file] %>
<adp:includeinto masterpagefile="file" />
<%@ IncludeInto

We don't actually provide a self-contained Tcl command syntax but if you really need to, you can use the code above to manually invoke the fancy tag

If onlycontentid querystring/formfield is present and not blank,
don't pour into master page and output only the requested content block.
Use ?onlycontentid=%20& (trailing & required when used in a query string)
to reference the unnamed content block.
If you need to manually ns_adp_parse a content page and have it return just
one content block, do this prior to ns_adp_parsing:
am_set iupdate [ns_getform] onlycontentid contentPlaceHolderId

Otherwise, this essentially does ns_adp_trunc; ns_adp_include file.
Your directive/tag can include additional attributes that will be passed as named arguments to the master adp file. The attribute-value pairs will be passed in the same order as they are in the tag (including the masterpagefile attribute), plus the last two arguments will be the string "attributes" and the id of the ns_set containing the attribute-value pairs.
So the adp can either access the attributes via
[am_set iget [lindex [ns_adp_argv] end] attribute ?defaultValue?] (which is case insensitive) or by using the am_adp_bindArgsToArray utility (see below)

Note that we expect that future versions will include attributes that control ns_adp_include caching behavior.

<%= [am_adp_content get ?contentPlaceHolderId? ?defaultContent?] %>
<adp:contentplaceholder ?id="contentPlaceHolderId"?>
<%@ ContentPlaceHolder

The Tcl command retrieves the previously stored content block or " " (space)
if undefined, unless defaultContent is not blank in which case it's returned if
the content block is undefined.
Use "" in contentPlaceHolderId for the default/unnamed content block
if you want to specify defaultContent with the Tcl command.
The declarative syntax does not support specifying the defaultContent
and returns a null string when the content block is undefined.

Note that contentPlaceHolderIds are case-sensitive (ids and class names are supposed to be case-sensitive per HTML 4.01 spec).  Fancy tags are also case-sensitive (though this may be a bug in AOLserver), the @ directives are not.  In either fancy tags or @ directives, the attribute names are not case-sensitive.

Optional Utility Commands:

am_adp_content set contentPlaceHolderId content

Not generally needed but allows one to explicitly
set/override the content block contents.
Use "" in contentPlaceHolderId for the default/unnamed content block.

am_adp_content getcurrentid

Returns the contentPlaceHolderId of the current block
as previously set with start or setcurrentid.
Returns a blank string if the id is undefined

am_adp_content setcurrentid contentPlaceHolderId

Allows a content block to override the contentPlaceHolderId
into which it should be placed in the master page.
Note that both start and end clear this setting
even if contentPlaceHolderId is omitted in their calls,
so this must be called after start if it's present and before end.

am_adp_bindArgsToArray arrayName

Store named parameters and their values passed to an adp in an array
this adds to the array keeping any previously stored defaults
in that array if the named parameter was not passed in.
Note that unlike typical Tcl proc named parameters,
key names here are not prefixed with -.

am_tagAttributes set tagNameOrUniqueId attribute value

Sets (overwrites) the attribute value for later retrieval by get

am_tagAttributes append tagNameOrUniqueId attribute value ?separator?

Appends the provided value to the previously set attribute value for
later retrieval by get. If separator such as ; or " " is provided
it is placed between the previous and new values but only if
it's not already present and if the previous value is not blank.
Creates the stored attribute if it doesn't already exist.
Returns the resulting value of the attribute.
Remember that ; must be enclosed in quotes as it's a special char in Tcl.


am_tagAttributes append tagNameOrUniqueId

Returns the list of previously stored valueless (aka boolean) attributes
Note on valueless attributes (i.e. DISABLED HIDEFOCUS NOWRAP)
- if you want to set these, you need to append them as values
to a "" attribute with a space for separator, i.e.:
am_tagAttributes append BODY "" DISABLED " "
(if you add the same valueless attribute twice, it will show
up twice in the output but this should not cause a problem)

am_tagAttributes get tagNameOrUniqueId attribute ?default?

Returns the currently stored value of the attribute or the provided default if the attribute doesn't exist or its value is blank.


am_tagAttributes get tagNameOrUniqueId

Returns all the stored attribute/value pairs in the format
for inclusion into an HTML/XML tag
(both keys and values are htmlquoted except single quotes are not)

To clarify, am_tagAttributes doesn't manipulate any tags. It simply
allows one to accumulate set/append directives into a global ns_set
and then to retrieve the results for placement into the tag.
tagNameOrUniqueId can be any arbitrary string to identify
the tag for which the directives are being accumulated / retrieved.

You would use this in Master Pages to build up the BODY tag as follows:
The page with content blocks would make calls like this:

am_tagAttributes set BODY bgcolor white
am_tagAttributes append BODY style margin:5px ";"
am_tagAttributes append BODY onload "OnloadHandler('name');"

Intermediate master pages can also do the same. The top-most
master page, which is the one that will retain its HTML and BODY tags
can also set attributes like this above the body tag and its actual
body tag would look like this:

<body <%=[am_tagAttributes get BODY]%>>

Example content & master using all three syntax forms and resulting output HTML
<!-- This is a sample content page - content.adp - tags here are in lowercase -->
global pagetitle
set pagetitle "My Page"
am_tagAttributes set BODY bgcolor white
am_tagAttributes append BODY onload "OnloadHandler();"
set mainid "main"
set masterpage "master.adp"
        <script language="Javascript" src="/inc/amutils.js">
        <adp:contentstart contentplaceholderid="head">
        <script language="Javascript">
            function OnloadHandler () {}
        <adp:contentend />
        <!-- Note that if you want to use xml-style self-terminated tags with
             a slash and no attributes you MUST have a space before the slash
    <body bgcolor="white" onload="OnloadHandler();">
        <%@ ContentStart%>
        <h2>Hello, visitor from <%=[ns_conn peerip]%>.
        Master Pages are cool!</h2>
        <%@ ContentEnd ContentPlaceHolderId="$mainid"%>
        <% am_adp_content setcurrentid LastModified
           # mtime in ns_adp_stats is some date from two years ago
           set file [file join [ns_adp_dir] [ns_adp_argv 0]]
           ns_adp_puts "This file was last modified on:<br>\n[clock format [file mtime $file]]"
           am_adp_content end
<!-- Note how you can use a tcl variable for the masterpage to include into: -->
<%@ IncludeInto MasterPageFile="$masterpage"%>
<script language="Javascript">
  //window.setInterval(SmartScroller_Save, 50);
<!-- You can put client-side script tag after IncludeInto and it will be the sent last-->
<!-- This is a sample master page - master.adp - tags here are in UPPERCASE -->
global pagetitle
if {![info exists pagetitle]} {set pagetitle "Master Page"}
am_tagAttributes append BODY onload "SiteWideOnloadHandler();"
            My Site | <%=$pagetitle%>
        <SCRIPT language="Javascript" src="/inc/amutils.js">
        <SCRIPT language="Javascript">
            function SiteWideOnloadHandler () {}
        <%=[am_adp_content get head]%>
    <BODY <%=[am_tagAttributes get BODY]%>>
                <TH colspan="2">
                    <H1>This is the master page.</H1>
                    <adp:contentplaceholder id="main">
                        Main Content Block goes here
                    <%@ ContentPlaceHolder id="LastModified"%>
<!-- This is the resulting output, note the UPPERCASE tags
     are from master and lowercase are from content -->
            My Site | My Page
        <SCRIPT language="Javascript" src="/inc/amutils.js">
        <SCRIPT language="Javascript">
            function SiteWideOnloadHandler () {}
        <script language="Javascript">
            function OnloadHandler () {}
    <BODY bgcolor="white" onload="OnloadHandler();SiteWideOnloadHandler();">
                <TH colspan="2">
                    <H1>This is the master page.</H1>
        <h2>Hello, visitor from
        Master Pages are cool!</h2>
        This page was last modified on:<br>
Wed Nov 16 2:48:22 AM Pacific Standard Time 2005


<script language="Javascript">
  //window.setInterval(SmartScroller_Save, 50);
Nested Master Pages

You can nest Master Pages. A master page can have a content placeholder in between its own start and end markers. Note that contentPlaceHolderIds are global across all nested Master Pages. It is possible for an intermediate master page to consume a content block of a given id and then to generate a new content block with the same id for further pouring into its master.  However, you have to be careful the sequence is such so that the end marker for such a block occurs after you get it (using the contentplaceholder tag or directive or with am_adp_content get) or it will be overwritten.

Also note that IncludeInto MasterPageFile url is relative to the page it's in, so in a nested master page, url is relative to IT, not the content page.


ADP Master Pages cannot use streaming.

@ directives work via a command we define, called just @ - you can create your own directives by defining a procedure (must be a Tcl proc, cannot be a C command) called am_adp_directive_directivename, where the directivename MUST be all lowercase.  The signature for such a proc is the same as for one that implements a fancy tag
(i.e. {attributesNsSetId} or {string attributesNsSetId} - in the case of the latter, string will always be blank)

Because @ is just a Tcl command, when using <%@ directives in an adp page, a space is required between @ and the directive name, whereas the space between % and @ is optional.  In addition, all the attributes must be on one line or the \ continuation must be used just like in any multi-line Tcl command invocation.  Again, the @ command and the declarative directive syntax requires our amattributes2set C module.


To help implement some of the optional utility functions, this module includes several of our generic AM API Tcl procs, which you may find useful elsewhere, so here is the  documentation for them:

am_set fromlist list ?setId?

returns setId in which it creates blank (not same as NULL)
-valued keys from the list (if no setId supplied, creates a new one)

am_set frommap map    OR    am_set frommap key1 value1 ?key2 value2? ...

Takes a map (i.e. a list in array get format) and returns a setId for a new
ns_set created from the map. Note that map can be in a single parameter or in multiple parameters.

am_set keys setId ?prefix? ?matchPattern?

Returns the list of keys in the ns_set, optionally prefixed with prefix,
optionally narrowed down to the keys matching the case-sensitive glob matchPattern.

am_set values setId ?keyMatchPattern? ?top?

Returns the list of values in the ns_set, optionally filtered to only
those KEYS that match the case-sensitive glob pattern in keyMatchPattern.
If top (an integer) is specified stops after that many values
(like SQL SELECT TOP 1 ...)

am_set ivalues setId ?keyMatchPattern? ?top?

Case-insensitive version of am_set values

am_set idelkeys setId key

deletes ALL case-insensitive occurances of key in ns_set setId

am_set iupdate setId key value ?usename?

Does a case-insensitive version of ns_set update. If usename is
non-"", the case version that was in the ns_set is preserved.
Note that both ns_set update and am_set iupdate move the updated
key to the bottom of the ns_set. This means that performing
an ns_set get after an ns_set update may not necessarily return
the value you just updated but instead another instance of this key
which was previously after the updated key and now appears before it!

am_set iupdateonly setId key value ?usename?

Performs an am_set iupdate ONLY if the key already exists

am_set iappend setId key value ?separator?

Appends provided string to the current value of the key
(key is matched case-insensitively, creates key if it doesn't exist).
Optionally, places the specified separator between the two strings.
Smart enough to not put one in, if one already exists at the end
of the current value or if current or specified value is blank.
Always preserves the case version of the key in the ns_set.
Returns the resulting value that is now in the key.

am_set imerge high low ?prefix? ?formatProc?

Does a case-insensitive version of ns_set merge. Any fields in
the low set are appended to the high set if a field with the same
key (case-insensitive) does not already exist in the high set.
Fields are added in a loop, so if there are multiple fields in
the low set whose keys differ only by case, only the first such
field will be included in the high set.
If prefix is specified, all keys in low are added to high as $prefix$lowkey
Each value can be optionally run through a formating/escaping proc
like am_unquoteAndQuoteHtmlNoAmp which you can specify in formatProc.

am_set get  setId key ?defaultValue?
am_set iget setId key ?defaultValue?

Wrappers to the ns_set get/iget commands that return the defaultValue
or "" if not supplied) if setId is "" (instead of throwing an error)
or the key does not exist in the ns_set or its value is blank.

am_set assign  setId key?key? ...
am_set iassign setId key?key? ...

Creates variables in the caller's context for each of the keys specified,
with values taken from the set.

am_set array setId ?keyMatchPattern? ?formatProc? ?sameKeyValueSeparator?

Like ns_set array returns the contents of the ns_set as a map in the array get format, only if the ns_set has multiple instances of the same key (case-sensitive), the values are concatenated with "," as the separator (can override in $sameKeyValueSeparator).
In addition, can narrow down to only keys matching the glob pattern in $keyMatchPattern.  Finally, each value can optionally be run through a formatting/escaping proc like am_quotehtml which you can specify in $formatProc.

For programming convenience, am_set put and am_set update pass through to ns_set with just the first 3 arguments


The following four procs are thin wrappers to am_dumpset (see below for full details on all the optional args):

am_set2attributes setid ?exclude? ?encode(default="quotehtml")? ?brace?  \

The reverse of our C command am_attributes2set -minimize
Converts an ns_set to a string of HTML tag attributes.
For example, if your ns_set contained:
    alt "Some \"alt\" text"
    height 60
    width 80
    "" "checked"
You would get:
    alt="Some &quot;alt&quot; text" height="60" width="80" checked

am_set2map setid ?exclude? ?encode? ?unquoteIntegers? ?addPrefix?

Simlar to ns_set array - converts an ns_set to a list suitable for Tcl's string map.
For example, if your ns_set contained:
    a 1
    b 2
    a 3
You would get ==> a 1 b 2 a 3

am_set2query setid ?exclude? ?fieldsjoin? ?separator? ?encode?  \
                     ?onEmptyKey(default="skip")? ?addPrefix?

Roughly the opposite of ns_parsequery (which takes a query string in
the form key1=value1&key2=value2 and returns an ns_set with those
key-value pairs). Converts an ns_set to a URL query string.
For example, if your ns_set contained:
    limit 20
    orderby date*
You would get ==> limit=20&orderby=date*
Note that the returned query string does NOT have the initial ? or &

am_set2xml setid ?exclude? ?fieldsjoin? ?separator? ?pairjoin? ?closePair?  \
                     ?onEmptyKey(default="skip")? ?addPrefix?

Converting an ns_set to an xml string.  For example, if your ns_set contained:
    firstName "John \"Little\""
    lastName Doe
    email ","
You would get:
 <firstName>John &quot;Little&quot;</firstName><lastName>Doe</lastName>
(above result would be all one line)
You can specify \n in fieldsjoin to separate each tag with a newline.
You can also specify \n\t in pairjoin and \n in closePair to get:

am_dumpset setid ?exclude? ?fieldsjoin? ?separator? ?encode? ?pairjoin? ?quote?  \
                     ?brace? ?unquoteIntegers? ?onEmptyKey? ?addPrefix? ?closePair? ?xml?

Returns "" if setid is "".  All the string defaults below are overwritten by blanks, so do not use "" for an argument to use its default - specify it explicitly (does not apply to integer/boolean agruments - "" is equivalent to 0 which is their default).

Arguments (the number of args in am_dumpset has gotten out of hand and using wrappers instead calling am_dumpset directly is strongly recommended):

setid The id of the ns_set to dump

exclude Specify a tcl list of keys to exclude from being put into the
result. Keys are treated case-insensitively. Note that if you
want to exclude all the ColValue.*, RowID.*, etc. type of keys
and just leave the keys without '.' in them, run your formdata
ns_set through: [ns_findset [ns_set split $formdata]] ""]

fieldsjoin The string that will be used to join the fields from the
ns_set. Default is '&'. Following new W3C recommendations,
fields in query string may be joined with ';' or even multiple characters.
(am_set2xml passes "" for this arg by default)

separator This proc automatically expands ns_sets collapsed by
am_uniqueset with $separator (',' by default).
(i.e. key=value1,value2 becomes key=value1&key=value2) If "" is
specified, no expansion will occur.

encode How to encode the key and value. The default, "", will not
perform any encoding. If encode contains 'quotehtml', HTML quoting
will be performed on both key and value. You can pass directives
to am_quotehtml for not quoting single or double quotes by
including them in this string (i.e. quotehtmlNosinglequotes)
Specifying any other non-blank value will perform an ns_urlencode
on both key and value.

pairjoin String to join each field's key and value with. Defaults to '='.
(am_set2xml passes "" for this arg by default)

quote Can be 0/"" (default), 1, or 2. If 1, each field's value will be
always wrapped with quotation marks. If 2, each field's value will be
quoted only they are not already present. Note that unquoteIntegers
has precedence over this argument.

brace Can be 0/"" (default) or 1. If 1, each field's key and value
will be enclosed in curly braces {} if needed.

unquoteIntegers Can be 0/"" (default) or 1. If 1, if a field's value
is an integer, any quotes surrounding it will be removed. The
quotes need not be evenly matched - any will be removed.

onEmptyKey What we should do if the key of the ns_set is "". The
default is no behavior change, and "" is used as the key. If
onEmptyKey is "usevalue", the value of the field will be used.
If onEmptyKey is "bypass", the value will be inserted directly
into the result string as the pair, ignoring any splitting,
encoding, bracing, or quoting options specified for that field.
If onEmptyKey is "skip", we'll pretend the field didn't exist.

addPrefix - string to prepend to each key. This string is NOT encoded or quoted.

closePair - string to append after value (this is really only useful
to add newlines/tabs between the value and the closing tag with xml)

xml Boolean - if True, key is formatted as <key> and each pair is closed with </key>
Note that you must still supply "" (or newlines/tabs) in fieldsjoin and pairjoin


am_boolvar string ?nullReturn?

Returns the opposite of [string is false string], except if string is null, returns whatever was provided in nullReturn or 0 by default.

URL Aliases | amattributes2set | ampools | ADP Master Pages

© Copyright 1996-2014 by and Solitex Networks. Legal Notices.
Creative Commons License Articles on this site are licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.