We're using Graffiti CMS as our CMS for Coders 4 Charities, a site that we built to spread the word about our charity event coming late April 2008. We needed to implement a registration page, so that developers and charities can register for the event. Graffiti is obviously very powerful and easy to use, but I had a hard time figuring out how to get user entry to work.
Here is my fundamental understanding of how things kinda work:
When you create a post, Graffiti will create a folder for your post, and create a default.aspx page. If you categorize the post, it will put your new page under the category sub-dir. When your default.aspx page renders, Graffiti will look for a .view file that matches the name of your post (for example, if your post name is Registration, the path will be [site root]/registration/default.aspx.) If Graffiti cannot find a registration.view file,it looks for default index.view and layout.view files, which tell it how to render the themes. If it finds your registration.view file, it will render according to the template in that view.
Graffiti has nice Contact and Comment out-of-the-box functionality, and it uses macros to code-gen the javascript in the view files. After some digging, I found out that the javascript function call is performing an Ajax.Request to a HTTP handler, which is compiled into the Graffiti.Core library. The button click event passes the path to the .ashx file, and the HTML <form> is serialized as the parameter collection passed to the handler. Somewhere in that core library the magic happens, the Request is processed, the contact, comment or your operation is processed, and the handler does a Response.Write to send back results.
So, these are the steps I took to implement a registration page on Coders 4 Charities (which, at this time, is not yet fully implemented)
1) Create your post. For this blog, we'll call it Registration. I put it in a category called registration. Put something basic in the body, it will be thrown out by your view file. UPDATED: 2/6/08: I was wrong, it will not be thrown out by the view file. It creates an actual post that is navigable if you put it in a category. I added it to the uncategorized category and checked all of the options that pretty much tell the post to not display.
2) Create the javascript file that will handle your button onClick event. I called mine c4c.js and I put it in the __utility\js folder (where the graffiti.js and prototype.js files are. NOTE: you need the protoype.js file to handle your Ajax.Request call!!).
function C4CRegisterDeveloper(url)
{
new Ajax.Request(url,
{
method:'post',
parameters: Form.serialize('register_form'),
onSuccess: function(transport)
{
var response = transport.responseText || "no response text";
alert(response);
},
onFailure: function()
{
alert("Something went wrong");
}
});
}
As you can see, this function (for now) simply throws the response from the Ajax.Request call into an alert() call, so I can make sure we made it round trip.
3) Create a new .view file with the name of your post (registration.view). Mine has a bunch of HTML <table>-based labels and text-boxes, followed by the submit button. I opted to not use the Graffiti macros for my submit button, because I didn't want to extend their macro library. Hard-coding works fine for me in this case. In addition, this view file contains the reference to my c4c.js file. Here is the markup for the registration.view file:
<script type="text/javascript" src="/__utility/js/c4c.js"></script>
<form action="$url" method="post" id="register_form">
<table>
<tr>
<td> <label class="form_field_pair" for="firstlast">Name</label> </td>
<td>
<input class="form_field" type="text" name="_firstname" id="_firstname" value="" tabindex="1" />
<input class="form_field" type="text" name="_lastname" id="_lastname" value="" tabindex="2" />
</td>
</tr>
<tr>
<td> <label class="form_field_pair" for="address">Address</label> </td>
<td> <input class="form_field" type="text" name="_addresslineone" id="_addresslineone" value="" tabindex="3" /> </td>
</tr>
<tr>
<td> </td>
<td>
<input class="form_field" type="text" name="_addresslinetwo" id="_addresslinetwo" value="" tabindex="4" />
</td>
</tr>
<tr>
<td> <label class="form_field_pair" for="citystatezip">City, State, Zip</label> </td>
<td> <input class="form_field" type="text" name="_city" id="_city" value="" tabindex="5" />
<select class="form_field" name="_state" id="_state" tabindex="6">
<option selected value="--">--</option>
...
</select>
<input class="form_field" type="text" name="_zipcode" id="_zipcode" value="" tabindex="7" />
</td>
</tr>
<tr>
<td> <label class="form_field_pair" for="email">Email</label> </td>
<td> <input class="form_field" type="text" name="_email" id="_email" value="" tabindex="8" /> </td>
</tr>
<tr>
<td> <label class="form_field_pair" for="phone">Phone</label> </td>
<td> <input class="form_field" type="text" name="_phonenumber" id="_phonenumber" value="" tabindex="9" /> </td>
</tr>
<tr>
<td> <label class="form_field_pair" for="phone">Cell</label> </td>
<td> <input class="form_field" type="text" name="_phonenumbertwo" id="_phonenumbertwo" value="" tabindex="10" /> </td>
</tr>
<tr>
<td> <label class="form_field_pair" for="talents">Talents</label> </td>
<td> <textarea class="form_field" id="_talents" name="_talents" cols="20" rows="5" tabindex="11"></textarea> </td>
</tr>
<tr>
<td> </td>
<td> <input type="button" name="Register" value="Register" id="Register" onClick="C4CRegisterDeveloper('/c4c.ashx');" /> </td>
</tr>
</table>
<div style="display:none;" id="contact_status"></div>
</form>
This can obviously be cleaned up, but hey, I'm still prototyping.
4) Create your Generic HTTP Handler. I created a C# Web Project, added a Generic Handler template, and named it c4c.ashx. VS will create the code-behind for you, and the end result looks like this (with my code added):
using System;
using System.Data;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
namespace C4CWeb
{
/// <summary>
/// Summary description for $codebehindclassname$
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class c4c : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
System.Text.StringBuilder myResponse = new System.Text.StringBuilder();
myResponse.Append(context.Request["_firstname"]);
myResponse.Append(" ");
myResponse.Append(context.Request["_lastname"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_addresslineone"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_addresslinetwo"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_city"]);
myResponse.Append(", ");
myResponse.Append(context.Request["_state"]);
myResponse.Append(" ");
myResponse.Append(context.Request["_zipcode"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_email"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_phonenumber"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_phonenumbertwo"]);
myResponse.Append("\r\n");
myResponse.Append(context.Request["_talents"]);
context.Response.Write(myResponse);
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
For now, all I am doing is making sure that I can parse off the elements in the serialized <form>. This is just proof-of-concept, and not clean by any means. I just want to pull the parameters off the context.Request object, assemble a string, and send it back to the caller to confirm the round trip. This is where you add your business logic. I still need to take these parameters and store them in my data source.
5) Push it up. I put the c4c.ashx file off the site root, and my C4CWeb.dll in the bin folder in Graffiti.
I think that should be everything! If I have forgotten anything, or if you have any questions, post a comment to this blog and I'll try to help.
Doug
This weekend, Jeff Julian and I have been given the privilege of recording podcasts at Microsoft's We Are Microsoft event, called the Charity Challenge Weekend. We are in Dallas, TX, the location of many wonderful things (light bulbs and elevators), most notably the extremely delicious Baja Burrito at Baja Fresh Mexican Cantina.
I have to admit that I was not too aware of what this event was all about, but after learning, I quickly jumped in with both feet. The Charity Challenge Weekend is all about pairing local Dallas charities with mostly local software developers with the purpose of building a quality software package, in 3 days.
18 charities have been selected for this event, and each charity has an average of 3 to 4 developers assigned to them. Each charity has its own individual set of requirements, from brand new web sites, to existing web site re-design, to custom Intranets, all the way to SQL upgrades. The implementation styles are as varied as the requirements, using technologies such as SubSonic, DotNetNuke, Grafitti CMS by Telligent, and Microsoft Office SharePoint Server.
We've had the opportunity to sit down and talk with, and record, about 8 or 9 of these charities today. We realize that these teams are extremely busy, and we appreciate the focus and dedication they are applying to this implementation.
One thing this event is really trying to stress, and that we are trying to capture in our podcasts is that in every community across the country (and beyond), there are developers who are looking to give back to their communities. Some of them have even mentioned helping out with local Habitat for Humanity projects, only to end up with bloody thumbs. On the opposite end of this spectrum are the charities, who work diligently to support their organization, and are having to cope with the frustration of an antiquated or under-developed record keeping, tracking and sharing system. This event is providing the opportunity for these charities to save their money, and for developers to save their thumbs, and apply their most valuable skills to a worthwhile cause. Sappy commentary insert here, but I really was filled with inspiration at many points during the day.
We've got gobs and gobs of photos of the event, with all of the developers hunkered down, churning out code (except us, for once! Although I did squeeze in a little XAMLicious WPF on the down-low), which will hopefully be posted soon. Jeff and I, when we reached critical tiredness, decided to take our roving camera out on the carpet for a tour of the facility, and the insanity that quickly began to unfold. We're still debating if this video is a suitable YouTube release candidate.
Jeff is posting all of the podcasts over on his blog, and you can find them here. For a list of all of the charities here this weekend, along with their mission statements and implementation requirements and proposals can be found here.
I hope to post a follow-up to this blog sometime tomorrow (after another delicious meeting between me and a Baja Burrito, of course).
Doug
This may be old news for some, but Firefox v2.0 does not support the display style "inline-block". I've read several blogs and articles on the subject, most of them suggesting using "-moz-inline-block" or "-moz-inline-stack" or "-moz-inline-box", none of which worked (and also managed to break IE support, subsequently).
Since I tend to fix my own problems (and quickly forget the solution when it creeps up again), I usually post a blog about it to remind myself in the future. In the event this blog helps anyone else out there, then gravy.
Turns out, "inline" works just fine and dandy. I should clarify a bit about the specific nature of my implementation. As the website I am working on is still written in ASP.NET 1.1, I can't use the cool new AJAX functionality. As a result, I've been forced into mixing ASP.NET and HTML controls on my pages, and accessing them through JavaScript.
I essentially wanted a checkbox that, when toggled, would hide and/or display a set of address controls.
The code looks like this:
function USHORAddress()
if (Form1.chkUSHORAddress.checked)
{
document.getElementById(" <%= lblHORCity.ClientID %>l").style.display = "inline";
//document.getElementById(" <%= lblHORCity.ClientID %>").style.display = "-moz-inline-block";
//document.getElementById(" <%= lblHORCity.ClientID %>").style.display = "-moz-inline-stack";
//document.getElementById(" <%= lblHORCity.ClientID %>").style.display = "-moz-inline-box";
}
else
{
document.getElementById(" <%= lblHORCity.ClientID %>").style.display = "none";
}
}
Let me know if this helps anyone!
Doug