Short datajs walk-through
For today's post, I simply want to give you a walk-through on how to create a web application that uses datajs from scratch. We'll be doing the whole thing - server database, middle-tier model, OData service, and web pages, so now would be a good time to grab some coffee.
To begin, I'll create a new database with some sample data just from the command prompt, which is easier than explaining in a blog how to go through the IDE motions.
C:\>sqlcmd -E -S .\SQLEXPRESS
CREATE DATABASE DjsSimpleDb;
GO
USE DjsSimpleDb;
GO
CREATE TABLE Comments (
CommentId INT IDENTITY(1, 1) PRIMARY KEY,
Author NVARCHAR(128) NOT NULL DEFAULT '(unknown)',
CommentText NVARCHAR(MAX) NOT NULL
);
GO
INSERT INTO Comments (Author, CommentText)
VALUES ('Marcelo', 'Life is like a chocolate value type held in a reference type.');
INSERT INTO Comments (Author, CommentText)
VALUES ('Marcelo', 'My last comment was pretty awful.');
INSERT INTO Comments (Author, CommentText)
VALUES ('Asad', 'See the kind of stuff I have to put up with?');
GO
Now, create a new web application project, DjsSimple.
Add a new ADO.NET Entity Data Model, and generate one from the DjsSimpleDb database. I'm naming the item DjsSimpleModel.edmx.
Next, I'm adding a WCF Data Service and naming it DjsSimpleService.svc, and touching it up as follows.
using System.Data.Services;
using System.Data.Services.Common;
namespace DjsSimple
{
public class DjsSimpleService : DataService<DjsSimpleDbEntities>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Comments", EntitySetRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
}
Great, now I will drag the datajs-0.0.1.js file to the Scripts folder under the DjsSimple folder in the Solution Explorer window.
Next, it's time to create our sample application. All we will do is show existing comments and allow comments to be posted. Here is all the code required for Default.aspx.
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="DjsSimple._Default" %>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
<style>
.error-area { border: 1px solid red; }
</style>
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<h2>Welcome to my comments app!</h2>
<div><button id="refreshCommentsButton">Refresh Comments</button></div>
<div><button id="addCommentButton">Add a new comment</button></div>
<div>Author: <br /><input id="authorBox" type="text" value="Me" /></div>
<div>Comment: <br /><input id="commentBox" type="text" value="My witty comment." /></div>
<div id="commentsArea"></div>
<script src="Scripts/jquery-1.4.1.js" type="text/javascript"></script>
<script src="Scripts/datajs-0.0.1.js" type="text/javascript"></script>
<script type="text/javascript">
OData.defaultError = function (err) {
$("button").attr("disabled", false);
$("#commentsArea").addClass("error-area").text(err.message);
};
function simpleHtmlEscape(value) {
if (!value) return value;
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function refreshComments() {
$("button").attr("disabled", true);
OData.read("DjsSimpleService.svc/Comments", function (data) {
$("button").attr("disabled", false);
var text = "Comments:";
$.each(data.results, function () {
text += "<br /> " +
"<b>" + simpleHtmlEscape(this.Author) + "</b>: " +
simpleHtmlEscape(this.CommentText);
});
$("#commentsArea").removeClass("error-area").html(text);
});
}
function addComment() {
$("button").attr("disabled", true);
var request = {
method: "POST",
requestUri: "DjsSimpleService.svc/Comments",
data: { Author: $("#authorBox").val(), CommentText: $("#commentBox").val() }
}
OData.request(request, function (data) {
$("#commentsArea").removeClass("error-area").html("Comment posted, refreshing...");
refreshComments();
});
}
$(function () {
$("#refreshCommentsButton").click(function (ev) {
refreshComments();
ev.preventDefault();
});
$("#addCommentButton").click(function (ev) {
addComment();
ev.preventDefault();
});
});
</script>
</asp:Content>
Let's look at this bit by bit. The HeaderContent area is used by the ASP.NET web site template to hook up content for the document body, so we'll include the CSS for error messages there. We can also include scripts here, but I've decided to put them at the bottom of the page, to allow it to render faster.
In the actual content, we start with some HTML that presents the user interface for the page. There's a button to refresh comments, and one to add comments, with an input box for the author name and comment text each. Finally, there's an area where we will display comments, or an error message, if desired.
After the user interface, we pull in jQuery and datajs, and then we start the code for this web page.
The first function we see is assigned to OData.defaultError, and simply sets up the error handler that is used unless we specify one for each operation. Many web pages have a simple error handling strategy, commonly displaying an error message and reseting some state (in our case, we make sure our buttons are enabled). OData.defaultError gives you a convenient place to define this handler.
Next we write up simpleHtmlEscape, which is used to exemplify a good practice: whenver you have something that is supposed to be plain text, if you're assigning it to an innerHTML property (or you use jQuery's .html() function), make sure to escape it to make sure that data can't be "poisoned" to include unwanted HTML tags or even scripts.
After this, we have two functions that make up the interesting functionality. The first one reads all comments, builds up an HTML string, and assigns it to the resulting area. Notice that we simply iterate over the results of the array. We could have used some form of templating, but I didn't want to introduce yet another technique to this sample.
The second function creates a request to POST a new comment to the service, and then kicks off a refresh. Note that we simply create an object that looks like what we'd like the server to see and assign it to the 'data' property of the request we build - datajs takes care of making sure things look right for the server.
Note that in both functions, we take care to disable the buttons while the operation is taking place, so the user knows that there's no need to keep clicking, and to avoid re-entrancy problems where we might fire multiple requests by accident.
Finally, we simply wire our functions to the buttons, and make sure that FORM postbacks don't fire for ASP.NET.
Hit F5 to start the browser on the default web page, look at the initial comments, add some more, and play around with the code. If you run into any problems, just drop me a comment here.
Enjoy!
Comments
Anonymous
February 10, 2011
Interesting stuff, thanks for the post. Seems like this will be a fairly straight port to MVC as well. I believe that there is a new MVC3 object that tracks if an object is HTML encoded, and prevents double-encoding. Perhaps if interoping with this object is it best to just let the server encode the data, and do all validation there?Anonymous
February 10, 2011
@Chris - you are correct in that you can rely on the server to do this for you (although it will depend a bit on how you get MVC to produce the feed and such). The interesting bit here is that we're really moving data back and forth, not just HTML, so in this case the server runtime is WCF Data Services, which won't do any encoding/decoding. That said, it's useful sometimes to have the server store and serve pre-escaped HTML strings, which you know are always safe to include directly into a page. But then the server better sanitize everything, and you need to communicate "across layers" the fact that this is a pre-escaped string. I tend to think that using simple types and being careful about who does what is cleaner in the grand scheme of things, but I understand the appeal of the alternative approach - a "mistake" in that case is double-encoding, which is ugly but typically safe, whereas a "mistake" in the other case has a higher chance of being a security problem.