Home   ::   Links   ::   The How To

Zajax, as the AJAX term has been refered to by so many, is a Zope AJAX technique. I figure if someone else can "coin" a term, so can I. Last night, I brain-blasted the name Zajax! There will be much more to follow, but it is a technique I am adapting to, using the Dojo Toolkit as a client-side scripting engine, with simple XMLHttpRequest calls to Zope. My hope and intention of this site is to provide a simple guide, and community forum, sharing my experiences along my path of learning so that it may help others.

I do not intend to use XMLRPC or JSON or other protocols/services/techniques. For reasons I will post later, I will continue to pursue XMLHTTPRequest as is used by industry in general. I know many will disagree, but this is what I want to work on. You can build your own. Also, I do not intend for this to be a product or service, just a technique using existing technologies. Having said that, I can see the possiblity of building a Zope product using python to receive and respond to the client requests. We'll see though. I mainly just want to share my experience here.

~Greg

Please contact me through my main web site at: www.1stbyte.com.


Update: 06/06/07
NO More Zajax! At least no demo comments.

Well, I haven't done much of anything with this in a year and a half. But, I have had lots of comments, which hopefully has helped some people out there learning. Since my last update, in Nov. 05, I have move away from Zope to PHP5. My dicision to do this was not without a lot of thought, because I do love Zope. In an uber-short explanation, I chose PHP because of the huge amount of users/developers out there compared to Zope. And when the Zope developers moved to the Zope3, that made it harder on me to do my job. (I like Zope2!) What it really comes down to is money, because when I needed to solve a problem in PHP, almost always someone had already solved it. With Zope, this happend less of the time, and I believe that is due to the amount of people using Zope compared to PHP.

Don't get me wrong though, the Zope community is awesome! And I love some of the things I can do with Zope, that are just harder to do in other environments. I do have a few small project still running Zope2 apps, which is nice, because I can keep my hands on it once in a while. But, I have no desire to run Zope3.

How does this affect Zajax.net? Well, it means you have no "blog comments" demo to see in action, as I have no Zope server or host running it now. I have the zexp download and the instructions though (below). So I hope this still is useful to a few people out there. Enjoy. ~Greg

Update: 11/05/05
Using Dojo.io.bind to send specific (dynamic) arguments to a Zope DTML Method without a form

So... It's been almost a month! I have been real busy in my business and have not had a lot of time for this learning. However, because I started on some basics and created the simple Comments demo, it made it easy for me to try out some other quick easy tasks on a production system. COOL! Yes, I actualy place a simple Dojo app on one of my clients' systems. I am totally still learning this stuff, and as I have done so many times before, I jumped right in and went live on the app, without much testing.

The client application is an uber-mini-CRM and billing system. (built in Zope of course) They use it for tracking athletes and we wanted to add the ability to label each athlete with a "team." What I did was add Dojo and some "live" updates to a team listing. Very simple really, but what is so cool is that you can do the AJAX thing without loading a new page. (XMLHttpRequest) The client can Add a team, which sends the data to the db and returns a new list below. (just like the Comments demo here) Then, even more exciting, they can edit each team and sport without submitting the data! All they do is change the text and mouse-out. I just use an OnChange event and XMLHttpRequest sends the data to the server, then on success it displays a notification on the top of the page. All very cool stuff, and this will change the way I develop apps in the near future.

Also, I have had a concern about the loading of Dojo when it's not needed on many pages. It's a large download and does slow page loads. With Zope (I love Zope for this), it was sooooo easy to simply add a property to my DTML Document called: AllowDojo. And set it as a boolean type to True. Now, in my standard_html_header, or whatever DTML method I want, I can do a quick -- dtml-if hasProperty('AllowDojo') -- and a -- dtml-if getProperty('AllowDojo') -- and if it's True, load Dojo. This rocks!

Above is an image of the Teams mini-app. (maybe I shouldn't be calling it an App, but it's not a widget, maybe a module? Who cares? You get the point I hope.) And below is a snippet of code to update the teams.

			
function updcomplete(thisteam) {
	var teamname = document.getElementById(thisteam)
	teamname.style.fontWeight = "normal"
}
function updteam (tid,thisteam,thissport) {
	var teamname = document.getElementById(thisteam)
	var teamsport = document.getElementById(thissport)
	teamname.style.fontWeight = "bold"
	showmsgs(1,'Saving Team: ' + teamname.value)
	dojo.io.bind ({
		url: "/teams/teams_upd_model" ,
		content: {
			teamid: tid, 
			teamname: teamname.value,
			teamsport:teamsport.value
		},
		error: errh,
		handler: updcomplete(thisteam),
		method: "post",
		mimetype: "text/plain",
		transport: "XMLHTTPTransport"	
	})
	
	showmsgs(0,'Data saved. Team: ' + teamname.value + ' ' + teamsport.value)

}	

Now, to do this I had to create the teams list (the "teams_list_model" dtml method) that displays all the teams. I there, I added some simple dtml from zope to generate dynamic element ID's that are sent by an OnChange event. On the input tag for the team name I added this:

onchange="updteam(<dtml-var teamid>,'teamname<dtml-var teamid>','teamsport<dtml-var teamid>')"

On the Select input item, I did a similar thing. But on the input tag, I send to the "updteam" javascript function a couple arguments. 1. I send in the teamid. 2. The teamname+teamid (concatenated, so to create a unique element) 3. The teamsport+teamid.
Doing it this way I can update that particular record with only 1 call to the server.

Another cool thing I did is strictly a Zope thing. But, I liked it and thought I'd share. You see above how each drop-down (select-option list) has a listing of sports? Well, that is drawn from a lookup table, and I didn't want to call the sql over and over for each team listed. That would be extremely expensive and waste resources if the list gets too large. What I did was generate the list and dump it into a variable in the REQUEST namespace. Here's what I did:

			
<dtml-call "REQUEST.set('slist','')">
<dtml-in "sql.getsports()">
	<dtml-call "REQUEST.set('sid',sportid)">
	<dtml-call "REQUEST.set('s',sport)">
	<dtml-let x="'<option value=' + _.str(sid) + '>' + s + '</option>'">
		<dtml-call "REQUEST.set('slist',slist + x)">
	</dtml-let>
</dtml-in>

Then what we do is run <dtml-var slist> in the select tag.

<select id="teamsport<dtml-var teamid>" name="sportid:int" class="selectregister" 
	onchange="updteam(<dtml-var teamid>,'teamname<dtml-var teamid>','teamsport<dtml-var teamid>')">
	<option value="<dtml-var sportid>"><dtml-var sport></option>
	<dtml-var slist>
</select>

Well I hope this helps someone. I didn't have time to do a full How-to on this, so I do hope this can be understood without the benefit of the full demo. The main point I wanted to share though, was the "updteam" function above. Because in here I make the AJAX XMLHttpRequest using Dojo to the Zope DTML methods without a FORM tag. In the Comments demo/example, we use Dojo.io.bind to send the contents of the entire form. Here, we just send in specific items. We use: "url" to send to the server method that updates the record. "content" to specify the variables and their values. "handler" to display results. And "method" to specify that we want to "post" not "get" request to the server.

On a side note, I mentioned this briefly before, but the __package__.js files in Dojo are a pain. I love Dojo because it's so easy once you see it work, particularly when using the AJAX functions. However, the graphics functions are not always working and as easy to use, nor are there as many as say, Scriptaculous. In fact, the Scriptaculous effects were easy and cool! So, with no widgets available to me until I can figure out how to build my own Dojo package, or they fix the __package__ names, I am a little hesitant to continue using Dojo.

Dojo is pretty hot though. The dojo.io.bind functions are so easy, and in fact, are all I am really needing. The effects are nice, but not a necessity. Same with widgets. They will eventually make for an entire programming environment that can be used in many other scenarios. This is very appealing to me. So, I will play with Prototype.js and Scriptaculous, but I have a hunch that Dojo will be the way to go, later.

Well, no demo/how-to, but I hope to put up a working demo and a zexp to download in the next few weeks. If I have time. :) ~Greg

Update: 10/11/05

I have updated the How-to thanks to Allan Schmidt for kindly pointing out that it didn't work. :) I had some retarded errors in my scritp samples. Plus there is a sample zexp to download at the bottom of the page too, so you can import into Zope and see the whole thing up close. Anyway, enjoy!

If you have anything you would like to contribute, whether it be for other script libs, please email me a how-to, zipped up, with all the html setup, and I'll put it up here for you. I will list it with credit to you as well. Be sure when you include code samples that you fine/replace any < > characters with the correct ascii code. (view my source and you will see what I mean.) I am considering just adding a wiki here, but I kind of want to build some basic things myself, just for learning purposes, so I'll add anything manually for the time being.

Email me here: "retheoff at yahoo dot com"


Update: 10/10/05

Well, I stayed up waaaaaaay too late (5:30) this morning to get my first Ajax app running! YAY! It's very simple, but since I am learning, I'll take baby steps. Of course, it uses Zope, which is uber cool because I can leverage my current skills using DTML and Python. You can see what I built in the Comments link above. Go ahead, save a comment, let me know what you think. I'll be posting the code for all this soon. oh, I am using the Dojo Toolkit for this.

One of the things I like about using the DTML and ZSQL methods on Zope is that I can make any query I like to my SQL backend (MySQL 5 in my case). Part of what I didn't like about Ruby On Rails was that I could't use stored procedures or functions. At least, I could't find a way to do that, and the Rails community seems convinced that it's better not to use stored procedures and have the Rails framework do all the SQL for you. (not saying this is bad, it's just not what I want to do) So, the way I have done this in Zope is quite simple, I even separated my code in pieces, just for ease of use. I have a dtml-var tag that pulls in a "controller" full of the app javascript. I have a "view", which is the main DTML document. The view is setup standard to DTML coding, it has a standard_html_header and footer, the dtml-var view_controller method with all the javascript client code, and all the view forms and markup. The "model" is in the DTML methods that utilize ZSQL and generate responses. Plus, I have a "layout" in my standard_html_header and footers! It's not a true MVC model, but for my purposes, it works great! (the best part is that I can do whatever SQL I want! Sprocs and all!)

The drawback to my setup is that it ties me to Zope. It's not using XML, it's rendering HTML which fills in divs on the client. And, it's not a true templating setup, using DTML instead of ZPT (Zope Page Templates), but it works for small-time operations like me. (in fact, I prefer this way) Tim Morgan's React is a much better framework, but I prefer to use these standard Zope methods instead of a whole new framework just so I have the flexibility I want, while still using my old skillset. (I know, shows my ignorance, but I still prefer DTML.)


Update yet again: 10/9/05

The React framework from Tim Morgan is very cool! Like Ruby on Rails implemented in Zope! It has the MVC (Model, View, Controllers) separation, Python, ZPT, and SQL calls. Way cool!

However, I think what I would rather have, and this is stricly my own opinion, is just a Zope product (what I will call Zajax, if I build it) that performs the SQL calls/responses and maybe even ZODB requests, without all the MVC stuff. I can see the benefit of using that, and I am sure this will strike up a lot of debate, but it's not for me. Just my opinion, and may not be the best solution.

I think, like I stated above, what I will do is keep learning and testing, and possibly create a simple Zope product to handle that exact thing, only the SQL and/or ZODB interaction. If I do, I think it should also utilize the Zope authentication mechanisms for the same reasons Tim mentioned on his site.


Update again: 10/9/05

Wow! Amazing what a little extra research brings up! Tim Morgan has built a way cool wiki using Zope and AJAX! Awesome! At first, I couldnt find hardly anything on Zope and AJAX, but sure enough, there are others doing it, and much farther along too. Cool! Check his stuff out here:
ZiddlyWiki

Tim Morgan is the person who created the REACT Framework for Zope , which I will be investigating soon. Looks very cool! Thanks Tim, and nice work!


Update: 10/9/05

Yesterday I began working with the Dojo Browser Toolkit. It is another AJAX Engine/Framework/Toolkit (whatever you want to call it) like Prototype.js. I originally started with Prototype in combination with Rico.js, but I had some trouble getting things to work.(I did eventually) Because I am experimenting with all this anyway, I thought I would try Dojo as well.

Dojo seems to be quite a large toolkit with many features, including widjets. I got it working fairly quickly with Zope, but I had trouble with the widjets. One thing Dojo does that I like so far, it takes a small amount of code to connect and retrieve the data from Zope, slightly less than with Prototype and Rico. And even more impressively, I didn't need to return XML back, just a raw display of HTML/CSS. This might have it's advantages, maybe it might even help with performance in larger datasets. We'll see though, one thing I didn't like in Dojo, was that they named the packages starting with underscores. ( __package__.js) This will not work in Zope. But, maybe they are not needed, and I can do everything I need with the complete dojo.js package. Time will tell.

I also found another post by someone else working with AJAX and Zope HERE. He is using another library called Datarequester.js. I havent looked into that one yet, but someone else might find this useful.

And one last thing, I just ran into this: REACT Framework for Zope
Seems like something I should look into and in fact might be what I need. Except, I want to use a more broadly accepted Javascript library, like Dojo or Prototype. It is this, and the techniques I would like to use with my current skillset in Zope that I am writing about here on this web site.

I will be posting some how-to's eventually, and they will allow some comments by others.


The HOW TO - Zope and AJAX Technique

This is all done the way I like to do it, there are many other ways. Simple Zope architecture, already in use and established. Whatever sql backend you can run in Zope. And the Dojo Toolkit, utilizing the XMLHTTP request method, typical of AJAX applications. We could run XML-RPC or JSON, but that's not where I want to go with this. Also, you could easily do all this with ZPT or even Python Scripts, or even a special Zope Product. See the Comments for an example. The code below is what I used to start off learning and created for the Comments example.

This how-to is explained as if it were an MVC framework, but I realize it is in no way like it. It's just that it sort-of resembles it when the pieces are layed out in a certain way.

You have your Model (the data queries and methods which recieve the requests from Dojo.io.bind.) You have your View, which is the HTML and form. With Zope using DTML methods, you have your Layout as a page template. (not ZPT though, but you could do that if you wanted.) And you have the Controller, which is the Javascript logic running on the Dojo Toolkit. The Contoller simply is another DTML Method that is inserted when the page is requested from the browser by Zope.

The beauty of all this AJAX/XMLHTTP stuff is that we can make calls to the Zope server to send back pieces of data to display, without reloaded the page. Cool huh? In this example, the Contoller (Dojo.js functions) make the XMLHTTP requests to the Model on Zope. And in this example, the Model is just some DTML and ZSQL Methods.

Anyway, have fun! Hope this all makes sense! ~Greg

Connecting the Dots

First... Some quick notes about Dojo. You will need to download the Dojo Toolkit at dojotoolkit.org. At this time I have the latest version 0.1.0 setup. Once downloaded and unpacked, upload the dojo.js to your server. Then you can reference it from your forms and script, making the framework available.

<script type="text/javascript" src="path-to-dojo/dojo.js"></script>



Be aware that Dojo has a few versions, some have additional packages. You can download a "Kitchen Sink" version with everything if you want. In Zope, you may have to use that version if you want some of the addon packages because you will not be able to use the "__package__.js" files. (invalid name in Zope) The "Kitchen Sink" version puts all these into one large dojo.js file, so that is all you need. For this demo, you only need the standard (AJAX version) dojo.js. If I put in other features that change the demo, I'll post reference to it here.

You will be placing that line of code in your "View" later.

The Database table:
Create a table in a database - blogcomments - (this is part of a blog system) I am using MySQL here, so adjust accordingly.

CREATE TABLE `blogcomments` (
  `commentid` int(10) unsigned NOT NULL auto_increment,
  `commenter` varchar(45) NOT NULL,
  `datecreated` datetime NOT NULL,
  `comment` text,
  `isdeleted` tinyint(1) unsigned NOT NULL default '0',
  PRIMARY KEY  (`commentid`)
) ENGINE=MyISAM;


This could be any database as long as you have it working on Zope because you will be using ZSQL methods. (kind of makes a layer of data abstraction, sort of.) Make sure to setup your database connection object for your ZSQL methods.

Be sure you create the database connection object and set it on your ZSQL method. In my example, if I reference it, I am using an object called "cn_zajax".

The Model:
This is comprised of two elements.
1. The ZSQL method "sql_getcomments"

select * from blogcomments
order by datecreated desc
2. The DTML Method "getcomments"
This is what sends back the request from the client.
<dtml-in sql_getcomments>
	<dtml-if sequence-start>
		<table border=0>
			<!-- <tr>
				<td>ID</td>
				<td>Author</td>
				<td>comment</td>
			</tr>
			-->
	</dtml-if>
		<tr>
			<td class="commenthead"><dtml-var commentid></td>
			<td class="commenthead"><dtml-var commenter></td>
		</tr>
		<tr>
			<td colspan=2 class="commenttext"><dtml-var comment></td>
		</tr>
		<tr>
			<td colspan=2 class="commenttext"><br /></td>
		</tr>
	<dtml-if sequence-end>
		</table>
	</dtml-if>		

</dtml-in>

This is the ZSQL method to insert a new comment. "sql_inscomment" This particular example is for MySQL, so be sure to change according to your SQL server. (for example, I have "now()" to insert the date.) When you create the ZSQL method, be sure you add 2 arguments, 1 for "commenter" and the other for "comment". Save and test it in the ZMI.

insert into blogcomments
(commenter,datecreated,comment)
values (<dtml-sqlvar commenter type=string>,
now(),<dtml-sqlvar comment type=string>)
<dtml-var sql_delimiter>
select * from blogcomments 
where commentid = @@identity

And of course, the other half of the insert, the DTML Method ("inscomment") that will receive the data from the form via the XMLHTTP request and send back a reply. In this case, it simply sends back 1, the last inserted. If we can do that, then cool, it worked. If not, it will ERROR before calling the getcomments DTML method. (which is also called when the form first loads, but from this point forward, we will just use the same DTML method to save the record ("sql_inscomment"), and if success call the other DTML method "getcomments".)

<dtml-if comment>
	
	<dtml-in sql_inscomment>
		<dtml-var getcomments>
	<dtml-else>
		Error saving comment.<br />
		<dtml-var getcomments>
	</dtml-in>
<dtml-else>
	ERROR
</dtml-if>

The View:
This is a DTML Document, "comments"
This is the actual document with all your form and layout markup.
Notice below we add in the dojo.js framework. In this example, create a subfolder called "scripts" and put the dojo.js in there.

<dtml-var standard_html_header>

<script type="text/javascript" src="scripts/dojo.js"></script>

<dtml-var comments_controller>



<h4>Comments --</h4>
<span style="font-size:8pt;font-style:italic;">
This is an UBER simple Zajax app!
</span>
<form action="getcomments" id="getcommentsform" method="post">
</form>
<form action="inscomment" id="inscommentsform" method="post">
	<div class="button" id="showaddcomments" onclick="showaddcomments();" >Add Comment</div>
	<!-- <div class="button" id="getcontactlist" onclick="getcomments(this);">Get Comments</div> -->
	<div id="commentform">
		Your name:<br />
		<input type="text" id="commenter" name="commenter" value="" /> <br />
		<br />
		Comment:<br />
		<textarea id="comment" name="comment"></textarea>
		<div class="button" id="savecomment" onclick="savecomment();">Save Comment</div>
		<div class="button" id="hideaddcomment" onclick="hideaddcomments();">Cancel</div>
		<br />
	</div>
	
</form>
<div id="commentlist">
</div>
<script>
function pageload() {
	getcomments('comments');
	hideaddcomments();
	
	};	

pageload();
</script>

<dtml-var standard_html_footer>



This "View" includes several other, externally included items. They make up the "Controller" and "Layout".

The Controller:
The "Controller" is made up of all the Javascript code, which is what your client application will run. This is where all the Dojo Toolkit runs. Notice that this is all included as a DTML Method in the View above. "comments_controller" It needs to be called after the Dojo.js script, obviously.
I like running all this script separately, it's a lot easier when viewing the HTML code and this at the same time, instead of scrolling up and down constantly. And with Zope, it loads right in with as a DTML method transperantly to during page load.

<script type="text/javascript">

function savecomment(){
	dojo.io.bind({
	    load: rdrcomments,
	    error: errh,
	    formNode: document.getElementById( "inscommentsform" ),
	    mimetype: "text/plain",
	    transport: "XMLHTTPTransport"
	});
	hideaddcomments();
	var myfrm = document.getElementById('inscommentsform')
	myfrm.comment.value = '';
	document.getElementById('commenter').value = '';
};

function showaddcomments() {
	dojo.graphics.htmlEffects.fadeShow(document.getElementById('commentform'), 1000);	
};
function hideaddcomments() {
	dojo.graphics.htmlEffects.fadeHide(document.getElementById('commentform'), 1000);
		
};
function errh( type, error ) {
    var msg = "Something went wrong." +
        error.message;
    alert( msg );
}


function rdrcomments(type,data,evt) {
	var pinfo = document.getElementById("commentlist");
	pinfo.innerHTML = data
};
function getcomments(felm) {
	
	dojo.io.bind({
	    load: rdrcomments,
	    error: errh,
	    formNode: document.getElementById( "getcommentsform" ),
	    mimetype: "text/plain",
	    transport: "XMLHTTPTransport"
	});
	
};


</script>

Notice the dojo stuff here... dojo.io.bind.
This is used in both the savecomments and getcomments functions. It does this:
load: THE FUNCTION TO LOAD AFTER RECEIVING RESPONSE.
This will send 'data' to the rdrcomments funtction, which simple dumps the 'data' into the innerHTML of the "commentlist" div.
error: THE FUNCTION TO RUN ON ERROR
formNode: THIS PULLS ALL FORM ELEMENTS FROM THE SPECIFIED FORM
This is really interesting as it will grab all the form items and send the names and their data to the server. So, as you saw in the Model ZSQL Method above, you could do some error checking to make sure you have a form element name before processing. Also, check for authentication, stuff like that. (I don't have that set here.)
transport: USE THE XMLHTTPREQUEST TRANSPORT
Dojo.js has other transports, but I am mainly interested in the XMLHTTP requests to the server. Zope handles this with zero configuration, even through Apache Rewrite rules. Cool!

dojo.io.bind({
	    load: rdrcomments,
	    error: errh,
	    formNode: document.getElementById( "inscommentsform" ),
	    mimetype: "text/plain",
	    transport: "XMLHTTPTransport"
	});

The Layout:
The layout I use with DTML is to just use other DTML methods to insert as headers and footers, which may also include other methods for inserting navigation or other things. Here's the standard_html_header,but I kept out a bunch of stuff, just too keep things brief. (like css stuff you can set yourself.)

<html>
<head>
<title>Zope and Ajax - Zajax.net</title>
<style>
body {margin:0px;padding:0px;font-family:verdana;font-size:9pt;}
p {font-family:verdana;font-size:9pt;padding-top:10px;}
td {font-family:verdana;font-size:9pt;}
div {margin:0px;padding:0px;border:0px solid black;}
div.container {margin:0px;padding:0px;width:620px;}
.leftside {left:0px;width:200px;top:0px;position:absolute;text-align:left;
	height:700px;
}
div.bodycontainer {width:422;top:0;position:absolute;left:200;}
div.mainbody {font-family:verdana;
font-size:9pt;
width:400px;
border:2px solid #3366ff;
padding:10px;

}
.head {width:422px;text-align:center;position:relative;}
.foot {width:422px;text-align:center;position:relative;}
h2 {color:#3366ff;}

pre {overflow:auto;width:390px;border:1px dotted #e9e9e9;}

div.button  {
	cursor:pointer;
	cursor:hand;
	font-size:7pt;
	font-weight:bold;
	border:1px solid black;
	background-color:#3366ff;
	width:90px;
	margin:5px;
	padding:1px;
	text-align:center;
		
}
div.button:hover,div.button:focus {
	cursor:pointer;
	cursor:hand;
	background-color:#e9e9e9;
		
}
textarea {
	font-size:9pt;font-family:verdana;
	width:360px;
	height:150px;
	border:2px solid #3366ff;	
}
input {
	font-size:9pt;font-family:verdana;
	border:2px solid #3366ff;	
	
	
}

.commenthead {
	font-weight:bold;
	text-align:left;
	font-size:9pt;
	font-style:italic;
	background-color:#e9e9e9;
	padding:0px;
	spacing:0px;
	margin:0px;	
}
.commenttext {
	font-weight:normal;
	text-align:left;
	font-size:8pt;
	padding:0px;
	spacing:0px;
	margin:0px;	
}

</style>
</head>
<body>
<div class="container">
	<div class="leftside"> </div>
		<div class="bodycontainer">
			<div class="head"><h2><dtml-var title_or_id></h2></div>
			<div class="mainbody">

The last piece is the standard_html_footer.

			</div>
		</div>
</div>
</body>
</html>

Now just if all is going well, open your browser to http://path.to.your.server.com/yourworkingfolder/comments and this should open the app. Here is the ZEXP file if you want all the code to import into Zope. zajax_demo.zexp Just remember to change your database connection information on the ZSQL connection object. Hope this helps somebody! Enjoy! ~Greg


Links

Last Updated: 06/06/07 - 2005 1st Byte Solutions