Monday, October 18, 2010 6:46 PM
Kazi Manzur Rashid
Microsoft WebPage/WebMatrix and Ruby Sinatra
This is a continuation of my previous post. In this post, I will do a side by side comparison of Microsoft WebMatrix/WebPage with Ruby Sinatra. The reason I picked Sinatra because both Sinatra and WebMatrix can be used to develop web sites with very minimal effort.
For your reference the Sinatra site is hosted over to heroku(cloud hosting for ruby application) and the source codes are in github.
Lets start with the development model, in MS WebPage, you start with a page and put your application logic in the page intermixed with the markup, very similar to classic asp or php. For example, the following shows codes of the home page of the bakery:
@{
Page.Title = "Home";
var db = Database.Open("bakery");
var products = db.Query("SELECT * FROM PRODUCTS").ToList();
var featured = products[new Random().Next(products.Count)];
}
<h1>Welcome to Fourth Coffee!</h1>
<div id="featuredProdcut">
<img alt="Featured Product" src="@HrefAttribute("~/Images/Products/" + featured.ImageName)" />
<div id="featuredProductInfo">
<div id="productInfo">
<h2>@featured.Name</h2>
<p class="price">$@string.Format("{0:f}", featured.Price)</p>
<p class="description">@featured.Description</p>
</div>
<div id="callToAction">
<a class="order-button" href="@HrefAttribute("~/order", featured.Id)" title="Order @featured.Name">Order Now</a>
</div>
</div>
</div>
<ul id="products">
@foreach(var p in products){
<li class="product">
<div class="productInfo">
<h3>@p.Name</h3>
<img class="product-image" src="@HrefAttribute("~/Images/Products/Thumbnails/"+ p.ImageName)" alt="Image of @p.Name" />
<p class="description">@p.Description</p>
</div>
<div class="action">
<p class="price">$@string.Format("{0:f}", p.Price)</p>
<a class="order-button" href="@HrefAttribute("~/order", p.Id)" title="Order @p.Name">Order Now</a>
</div>
</li>
}
</ul>
But in Sinatra you start with a route, but unlike the ASP.NET MVC or Rails you do not have to declare the route, they are inlined, let me show you the sinatra version of the above code:
# Home
get "/" do
@title = "Home" # Page title
# Get all the products
@products = Product.all(:order => [:name.asc])
# Picking up a random product as featured
count = @products.length
@featured = count > 0 ? @products[rand(count)] : nil
erb :index # We are using erb as view engine
end
As you can see, in sinatra you start with a http method like Get, Post, Put, Delete, next you have to define the endpoint for which the associated code block will execute, in this case the above code will execute when the root of the site is requested. Now, in the code unlike the WebMatrix which uses plain sql, we are using Ruby DataMapper(there are various other alternates like ActiveRecord, Sequel, MongoDB, CouchDB for data access) to get all the products and among it we are picking up a random product as featured one. Once we are done we are using ERB as view engine to render the view. Like the data access there are also severeal alternates when it comes to view engines, among it Haml is the most popular one, but to make it simple I have decided to go with the ERB. Another nice thing of sinatra is that I can include my view in the same file where I am handling the route or I can even embed my view as inline like erb "Current Time: <%= Time.now =>". If I include the view in the same file the code will look like the following:
# Home
get "/" do
@title = "Home" # Page title
# Get all the products
@products = Product.all(:order => [:name.asc])
# Picking up a random product as featured
count = @products.length
@featured = count > 0 ? @products[rand(count)] : nil
erb :index # We are using erb as view engine
end
__END__
@@ index
<h1>Welcome to Sinatra Fourth Coffee!</h1>
<% if @featured %>
<section id="featured">
<figure><img alt="Featured Product <%= @featured.name %>" src="http://weblogs.asp.net/images/products/<%= @featured.picture %>"/></figure>
<div>
<header>
<h2><%= @featured.name %></h2>
</header>
<div>
<p>$<%= money(@featured.price) %></p>
<p><%= @featured.description %></p>
</div>
<div>
<a class="order-button" href="http://weblogs.asp.net/order/<%= @featured.id %>" title="Order <%= @featured.name %>">Order Now</a>
</div>
</div>
</section>
<% end %>
<% @products.each do |product| %>
<section class="product">
<header>
<h2><%= product.name %></h2>
</header>
<figure>
<img class="thumbnail" src="http://weblogs.asp.net/images/products/thumbnails/<%= product.picture %>" alt="<%= product.name %>"/>
<figcaption><%= product.description %></figcaption>
</figure>
<p>
<span>$<%= money(product.price) %></span>
<a class="order-button" href="http://weblogs.asp.net/order/<%= product.id %>" title="Order <%= product.name %>">Order Now</a>
</p>
</section>
<% end %>
But in the GitHub you will find that I have used separated view files instead of including in the same code file.
In the above code I have mentioned that it is using DataMapper, but I did not show you how it has been configured, here is the configuration of DataMapper, in the WebMatrix example you will find there is only one database table and when an order is received it just sends an email, but in the sinatra version i have extended it a bit more so that it stores the order in the database instead of sending email.
require "rubygems"
require "dm-core"
require "dm-validations"
require 'dm-migrations'
DataMapper::setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/bakery.db")
class Product
include DataMapper::Resource
property :id, Serial
property :name, String, :length => 64
property :description, Text, :lazy => false
property :price, Decimal, :precision => 12, :scale => 2
property :picture, String, :length => 256
end
class Order
include DataMapper::Resource
property :id, Serial
property :created_at, DateTime, :required => true
property :email, String, :length => 256, :required => true, :format => :email_address
property :address, String, :length => 1024, :required => true
property :quantity, Integer, :required => true, :min => 1, :max => 100
belongs_to :product
end
configure :development do
# Only Upgrade in development mode
DataMapper.auto_upgrade!
end
If you are familiar with Fluent NHibernate or Entity Framework Code First then the above is nothing different other than it is ruby code. Also check that we are embedding the validation rules in the property but I think the latest EF code first also has this feature via the Data Annotation. Since we have added all validation rules in the model when an order is received the form validation becomes absolutely easy:
post "/order/:id" do |id|
product = Product.get(id)
# Only save if product exists
if product
quantity = params[:quantity].empty? ? 0 : Float(params[:quantity])
@order = Order.new(:product => product, :created_at => Time.now.utc, :email => params[:email], :address => params[:address], :quantity => quantity)
if @order.save
redirect("/success/#{@order.id}")
else
@title = "Place Your Order: #{product.name}" # Page title
erb :order
end
else
# Show 404
throw_not_found
end
end
And the View:
and here is the WebMatrix version:
@{
Page.Title = "Place Your Order";
var db = Database.Open("bakery");
var productId = UrlData[0].AsInt();
var product = db.QuerySingle("SELECT * FROM PRODUCTS WHERE ID = @0", productId);
if(product == null){
Response.Redirect("~/");
}
if (IsPost) {
var email = Request["orderEmail"];
if (email.IsEmpty()) {
ModelState.AddError("orderEmail", "You must specify an email address.");
}
var shipping = Request["orderShipping"];
if (shipping.IsEmpty()) {
ModelState.AddError("orderShipping", "You must specify a shipping address.");
}
//If there is no error try to process order
if(ModelState.Count == 0){
// Mail Sending Code
}
}
}
As you can see that in the Sinatra version we are not doing any kind of manual validation, instead the DataMapper is taking care of it, these are done based upon the rules that we have setup in the model. When the order is saved we taking the user to the successful otherwise showing the user the same order page.
Here is the view of the order page:
<progress class="orderProgress" value="2.0" max="3.0">
<ol>
<li><span>1</span>Choose Item</li>
<li class="current"><span>2</span>Details & Submit</li>
<li><span>3</span>Receipt</li>
</ol>
</progress>
<h1><%= @title %></h1>
<form id="orderForm" action="" method="post">
<fieldset>
<legend>Place Your Order</legend>
<img class="thumbnail" src="http://weblogs.asp.net/images/products/thumbnails/<%= @order.product.picture %>" alt="<%= @order.product.name %>"/>
<ul>
<li>
<label for="email">Email Address:</label>
<input type="email" name="email" id="email" value="<%= @order.email %>" placeholder="Type your email" required="required" autofocus="autofocus"/>
<%= field_validation(@order, :email) %>
</li>
<li>
<label for="address">Shipping Address:</label>
<textarea rows="4" cols="20" name="address" id="address" placeholder="Type your address" required="required"><%= @order.address %></textarea>
<%= field_validation(@order, :address) %>
</li>
<li>
<label for="quantity">Quantity:</label>
<input type="number" name="quantity" id="quantity" min="1" step="1" max="100" value="<%= @order.quantity %>" required="required"/>
x <span id="price"><%= money(@order.product.price) %></span> = <output id="total" for="quantity,price">$<%= money(@order.product.price * @order.quantity) %></output>
<%= field_validation(@order, :quantity) %>
</li>
<li><button type="submit" class="order-button">Place Order</button></li>
</ul>
</fieldset>
</form>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script>!window.jQuery && document.write('<script src="js/jquery-1.4.2.min.js"><\/script>')</script>
<script>
$(function () {
var price = parseFloat($("#price").text()).toFixed(2);
var total = $("#total");
var quantity = $("#quantity");
quantity.change(function () {
var newQuantity = parseInt($(this).val());
if (!newQuantity || newQuantity < 1) {
quantity.val(1);
newQuantity = 1;
}
else if (newQuantity.toString() !== quantity.val()) {
quantity.val(newQuantity);
}
total.text("$" + (price * newQuantity).toFixed(2));
});
});
</script>
Both in the handler and in the view you will find few methods like throw_not_found, field_validation which are not built-in methods, those are some helper methods, to create helper methods you just have to put those methods in a helper do and end block like the following:
helpers do
def throw_not_found()
raise Sinatra::NotFound
end
def money(value)
sprintf("%.02f", value)
end
def field_validation(target, field)
"<span class=\"field-validation-error\">#{target.errors[field][0]}</span>" unless target.errors[field].empty?
end
end
I think I have shown most of the codes of this application and explains how it works, if the above interests you I would suggest you download the code from the github and check it side by side. Overall I think Sinatra is superior comparing to WebMatrix/WebPage when it comes to writing clean code and separating parts of your application and yet maintaining the simplicity and minimalism. I wonder if Microsoft did evaluate it before choosing the asp/php path and I would definitely like to see similar features baked into it so that real professional can consider it instead of only hobbyist.
Filed under: Ruby, webmatrix, sinatra