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="/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="/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="/images/products/thumbnails/<%= product.picture %>" alt="<%= product.name %>"/>
        <figcaption><%= product.description %></figcaption>
    </figure>
    <p>
        <span>$<%= money(product.price) %></span>
        <a class="order-button" href="/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 &amp; 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="/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.

Shout it

6 Comments

  • Very interesting, and a perfect illustration as to why Microsoft went the PHP/classic ASP route for beginners and hobbyists. Low concept count is key.

    WebMatrix is not targeted at real professionals. ASP.NET MVC is.

  • the fact is you can do the exact same thing both in WebMatrix and In Ruby Sinatra.You can use whatever API that exists in C# and .NET , it means model validation , linq, ect ... so i reallyt dont see the issue here ...

  • Good Post !!

    Its really ANNOYING to see microsoft react SOooo late to the development trends and good practices,

    MS just keeps pushing toys at the developers to make them busy,

    and this is why when a .NET developer sees what RoR / Sinatra can do ,, it blows the mind off.


    Recently MS released NUPack and I wonder what took years to get this NEAT (copycat) to developers. It should have been there years before ,, like ruby gems

  • @Mike: There is a big gap between the pure static page and ASP.NET MVC and I wish WebMatrix can filll that.

    Both Beginners and Hobbyist only remains Beginners/Hobbyist for a time being and you should not give them something which has greater chance of promoting bad practices. Sinatra is not at all a MVC Framework but the transition to any mvc framework feels very natural.

  • @camus: You are missing the point, the basic difference in Sinatra I am not intermixing my presentation code with my application logic. WebMatrix does come with few helpers but I have not seen any which does the automatic validation.

  • @neo x: Agreed

Comments have been disabled for this content.