It is often said that ASP.NET MVC was inspired by Rails. What better way to test that assertion than by writing the Nerd Dinner ASP.NET MVC application in Rails? In this article, I’ll take you through the steps I used to get Nerd Dinner up and running in Rails. A few points to keep in mind:
If you happen to be a .NET guy like me and you happen to be tasked with writing or porting from a Rails environment, this article should be helpful. I implemented every major feature in Nerd Dinner (Ajax, JSON, jQuery, etc.) in what I have called Rails Dinner (a play on words re: Nerd Dinner). With that in mind, let’s get to porting Nerd Dinner to Rails!
A Quick Word About Tests
If you are familiar with a typical Rails project, tests are an integral part of the process. Just as Rails was built from the ground up with MVC in mind, unit tests are right there front and center.
However…..
Being that this is my first Rails application with the initial goal of building an application based on an existing ASP.NET MVC exemplar, I opted to set testing aside. In concept, tests in Rails are just like tests in ASP.NET MVC. For demonstration purposes, I will illustrate the mechanics of defining and running unit tests. You will see that the Rails framework provides facilities for test fixtures in order to place a database in a known good state. By default, Rails will configure a separate test environment so as not to disturb your production and development environments. Later in this article, I will discuss how Rails manages the test, development and production environments.
Testing, whether it is unit or integration testing is a first class citizen in the Rails environment. Under normal circumstances, in a TDD (Test Driven Development) scenario, you would:
- Write a test (that would initially fail)
- Write code
- Run the test/modify the code as needed until the test passes
- Repeat the process (Red, Yellow, Green Development)
When I first created the Nerd Dinner Rails project, it created a test folder. There are test fixtures and stub test classes ready to be used. You can find a nice overview of testing in Rails here: http://guides.rubyonrails.org/testing.html
If you continue to go down the Rails route, and if you are a fan of BDD (Behavior Driven Development), you will definitely want to check out Cucumber: http://github.com/aslakhellesoy/cucumber. You can find a nice overview of Cucumber here: http://www.rubyinside.com/cucumber-the-latest-in-ruby-testing-1342.html.
Cucumber provides the ability to write tests in the language of the domain. That, in a nutshell, is my mea culpa as it pertains to testing and how integral it is re: idiomatic Ruby on Rails.
Nerd Dinner, ASP.NET MVC and Rails - the Intersection
Nerd Dinner is not defined so much by its data, but rather, by the way it illustrates the ASP.NET MVC implementation. The business problem Nerd Dinner addresses is fairly trivial. There are any number of ways you can keep track of events and attendees. What is interesting about Nerd Dinner is how it illustrates, in a very simple way, the power and flexibility of the ASP.NET MVC Framework. In very short order, Nerd Dinner, in addition to the ASP.NET MVC Framework itself, illustrates how to incorporate jQuery, Ajax, JSON and forms authentication in an ASP.NET MVC application.
It is often said that ASP.NET MVC was inspired by Rails. Because I had not looked at Rails, I never understood or appreciated that point. While attending the 2009 DevLink Conference in Nashville, TN, I had an eye-opening experience. At the closing session, Richard Campbell and Carl Franklin recorded a live session of .NET Rocks! (http://www.dotnetrocks.com/default.aspx?showNum=476). The main topic of the show discussed whether software development has become too complicated.
Getting back to DevLink, during the Q and A portion of the .NET Rocks! episode, a guy named Leon Gersing came to the microphone and said, among other things, that the panel was not qualified to render it’s opinions on Ruby and Rails for the simple reason that they had not worked in that environment. I thought, “Wow, how refreshing is this?” Subsequently, Carl and Richard hosted Leon as a guest on .NET Rocks! (episode 482 - “Leon Gersing is Having a Love Affair with Ruby,” http://www.dotnetrocks.com/default.aspx?showNum=482). You can find Leon’s Web site at www.fallenrogue.com and follow him on Twitter @FallenRougue.
Leon’s comments at DevLink and his .NET Rocks! appearance inspired me to check Rails out. In the summer and fall 2009, I had already written a number of blog posts concerning Nerd Dinner. I had already integrated NHibernate/Fluent NHibernate, Structure Map and various jQuery plug-ins into the base Nerd Dinner application. With a good understanding of Nerd Dinner, I figured, why not? Why not re-write Nerd Dinner in Rails from the ground up, using the ASP.NET MVC version as the specification! How hard could it be? As it turned out, after about a week, it was not that difficult. To be candid, I didn’t develop a Rails app of high pedigree given that tests were not central to the effort. Nevertheless, the job got done!
The process had challenges. Those challenges however, were borne more out of my “noob” status re: Rails. As for Ruby, let’s just say that it is now my favorite language! With all of that in mind, I would like to take you through my journey of putting Nerd Dinner in Rails.
Getting Up and Running with Rails
Chances are you don’t have Rails installed. In order to install Rails, you must first install the Ruby language. So far, I have simply referred to Rails as Rails. Technically, what I’ll show you is Ruby on Rails. I suppose the .NET equivalent would be <<insert language here>> on ASP.NET MVC. Ruby is the language and Rails is the Web framework that provides the MVC infrastructure. Whereas in ASP, MVC was bolted on via Microsoft.Web.Mvc, Rails was built from the ground up with MVC in mind.
The best place to get up and running with Rails is to visit http://rubyonrails.org/. Once you go to the download page, you will have three tasks to complete:
-
Download Ruby: The current version is 1.9.1. However, 1.9.1 has only been out for a short time. NOTE: Ruby 1.9.1. DOES NOT work with Rails version 2.3.4, which I used for this article. I wrote Rails Dinner with Ruby version 1.8.6, which has a one-click Windows installer that makes it a snap to install. You can use either version 1.8.6 or 1.8.7 of Ruby to work with this article. The Windows Installer takes care of making sure you’ve added Ruby to your path.
-
Download RubyGems: RubyGems is the standard package manager for Ruby. When extending Ruby, you do so via a gem. Once you’ve downloaded RubyGems, you install it by issuing the following command at the command prompt: ruby setup.rb.
-
Install Rails: This is not a misprint and there is nothing for you to download manually. Just this process alone may be enough to get you enthused about Rails. Rails is a gem you install in order to extend Ruby’s functionality. In order to install the Rails gem, or any other gem for that matter, you issue this command from the prompt: gem install rails -v=2.3.4. If you don’t specify the version, Rails 3.0, just released to beta will be installed. Rails 3.0 requires Ruby 1.8.7 or later.
If all worked correctly, you now have Ruby and Rails installed. In order to verify the versions, you can issue the commands ruby -v and rails -v from the command prompt. Figure 1 illustrates what you should see.
For Nerd Dinner on Rails, you need to install two additional gems: the GeoKit gem and the Ruby SQLite 3 gem.
Looking at Nerd Dinner’s geography capabilities, most of that is handled via Microsoft MapPoint. If you look in the Map.ascx partial view, you will find the following script source: **http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.**2. This facilitates the map display in Nerd Dinner. The Nerd Dinner on Rails app uses the same mapping resource. However, when it comes to calculating the distance to Dinners from search location, the original Nerd Dinner used a SQL Server stored proc that took pairs of latitude/longitude coordinates in order to provide the distance in miles. Fortunately, there is a gem called GeoKit that can handle this functionality. The following code will install these gems to your system - making them available to your Rails-based applications:
gem install geokit
I’ll get into the specifics of implementing the GeoKit Gem later on.
By default, Rails supports the SQLite 3 database. Rails also supports SQL Server, MySQL, Oracle, and others. To keep this example as simple as possible, I’ll stick with SQLite 3. Before you can install the SQLite Gem, you need to install SQLite 3. You can find everything you need to install SQLite 3 here: http://www.SQLite.org/.
Once you have installed SQLite 3, you can then install the gem:
gem install SQLite3-ruby
You’ll also need to use a plug-in. In the ASP.NET MVC version of Nerd Dinner, authentication is handled via ASP Forms Authentication. In order to duplicate the functionality you need to employ some method of authentication. Fortunately, there is a plug-in for that! Once you have the basic Nerd Dinner app up and running, the Restful Authentication plug-in will be installed.
Creating the Base Nerd Dinner App on Rails
With Ruby, Rails, and GeoKit installed, I can now show you how to create the Nerd Dinner application. It’s a pretty simple process to create the base application. First, create a directory. For this example, I called mine RailsApps. In that directory, type: rails nerddinner. You will see the cursor blink for a few moments, and then just like that, you have the guts of your new Nerd Dinner Rails application. Figure 2 illustrates the output you should see. Figure 3 illustrates the directory structure Rails created for the new application.
OK, so now what? At this stage, your application isn’t capable of much. But, there is some functionality. When you installed Rails, you also installed a local Web server called WEBrick. From your railsapps\nerddinner directory, issue the following command:
ruby script\server
This command starts-up the local WEBrick Web server. By default, WEBrick listens for requests on port 3000. Figure 4 illustrates what you should see after you start the Web server and open a Web browser, navigating to http://localhost:3000.
So then, where does this default page come from? Rails provides that as well. Another directory which you will become familiar with is the public directory. Figure 5 illustrates where the index.html page is located. The public directory is the home directory of the application.
This is really no different than a newly created ASP.NET MVC app. Figure 6 illustrates what you get for free with a new ASP.NET MVC App.
What if you try to navigate to a Dinner? Figure 7 illustrates the exception that is thrown when you attempt to navigate to http://localhost:3000/Dinners.
Considering that you have not created a Nerd Dinner database yet, much less a Dinners table or entity, you shouldn’t expect to be able to navigate to a Dinner. The exception, however, is very instructive. Just like ASP.NET MVC, Rails relies on a routing hash in order to direct traffic. When you make a request in ASP.NET MVC that does not match a route, you may see an error similar to Figure 8. In Rails and ASP.NET MVC you can easily deal with these scenarios.
With the framework basics in place, let’s take a look at Nerd Dinner on Rails, with Nerd Dinner on ASP.NET MVC as the guide.
Adding Dinners and RSVPs to Nerd Dinner on Rails
Like the ASP.NET MVC version, the Nerd Dinner application doesn’t need a complicated database. It has only two entities:
- Dinners
- RSVPs
The relationships are fairly straightforward as well:
- A Dinner has 0:N RSVPs
- An RSVP has 1 Dinner
Earlier you installed SQLite 3 and the Ruby SQLite 3 gem. In order to move the process along I will use scaffolding to create the Dinner and RSVP objects (models, views, controllers, routing, etc.). Scaffolding is a way to automatically generate models, views and controllers based off the data structures. In addition, Rails creates the files that will hold unit tests. The resulting components are just a starting point. Without exception, these files will always need to be modified.
Scaffolding also creates the database migrations scripts - which is itself worthy of its own article. I won’t go into the specifics here, but it is worth your time to check out how Rails provides database versioning.
The following code scaffolds the Dinner objects:
ruby script/generate scaffold Dinner
id:int Title:string Eventdate:datetime
Description:string Hostedby:string
Contactphone:string Address:string
Country:string
Latitude:string Longitude:string
The following code scaffolds the RSVP objects:
ruby script/generate scaffold Rsvp
id:int Attendeename:string Dinner:references
Figure 9 illustrates the results of these two commands. Note that for the RSVP scaffold, the directive Dinner:references is present. This directive tells Rails that it has a foreign key relationship with Dinner. Also, notice that the primary key for each table is called id. This gets to what is perhaps the most important concept in Rails: Convention over Configuration.
If you look in the apps folder, under the models, views and controllers folders, you will see a number of files to support Dinners and RSVPs. Take a few moments to look through the files. If you noticed, relative to models in ASP.NET MVC, there is almost no default code in a Rails Model. In Rails, the framework reads the database for its entity definition. Notice that the models are based on ActiveRecord.
ActiveRecord is actually a design pattern outlined in Martin Fowler’s Book Patterns of Enterprise Application Architecture*.* In a nutshell, ActiveRecord is Rails’ take on Object Relational Mapping (ORM). If you are like me and you dove head first into NHibernate, LINQ to SQL, etc., this will be a big change for you. In the Rails Framework, ORM and data persistence is just there. It is simply a service provided by the Rails framework, nothing more and nothing less. Recently, Rob Connery, who is a former member of the ASP.NET MVC Team, wrote a very thoughtful blog post on ORMs and data persistence. You can find Rob’s post here: http://blog.wekeroad.com/2009/12/26/thoughts-on-ef-vs-nhibernate-redux. Rob also wrote SubSonic (http://subsonicproject.com/), which is a .NET implementation of the ActiveRecord pattern.
Beginning with Rails 3, with the incorporation of MERB (http://rubyonrails.org/merb), instead of only having ActiveRecord, you can choose an ORM. MERB is an ORM-agnostic MVC framework. With its inclusion in Rails 3, you can continue to use ActiveRecord, or, you can choose other ORMs including Sequel and DataMapper.
With the models, views and controllers in place, are you ready to start creating Dinners? Try and navigate to http://localhost:3000/Dinners. As Figure 10 illustrates, you aren’t quite there yet. If you look in the db\migrate folder, you will find a clue to a very important step that you haven’t completed yet. You need to migrate your database changes. In this case, the migration involves creating your database entities.
To migrate the database changes, you’ll use another tool that is written in Ruby: Rake. Rake is a build tool that allows you to run tasks. In this case, you have two database migration tasks that you need to run. To facilitate this migration, you need to run the following command from the prompt:
rake db:migrate
This command tells Rake to run the scripts in the db\migrate folder. There is actually quite a bit going on here, especially with regard to how database versions are managed, but those details are beyond the scope of this article. Hopefully, you can already see there is quite a bit of power and flexibility at your disposal. Figure 11 illustrates the results of the db migration.
OK then, with the database in place, are we good to go yet? Go ahead and try to navigate to http://localhost:3000/Dinners. No error this time! As Figure 12 shows, it’s not pretty, but it is almost 100% functional. Go ahead and click the new link. Figure 13 illustrates the error.
If you recall, when scaffolding the RSVP entity, you indicated that an RSVP referenced a Dinner. In other words, the RSVP is the many side of a one-to-many relationship with Dinners. The following is the code for the RSVP model:
class Rsvp < ActiveRecord::Base
belongs_to :Dinner
end
As you can see, an RSVP belongs to a Dinner. All you need to do is modify the Dinner Model so that it knows that it has many RSVPs:
class Dinner < ActiveRecord::Base
has_many :Rsvp, :dependent => :destroy
end
With the added code, Rails is not only aware of the relationship when a Dinner is deleted, its related RSVP data will be deleted as well.
Figure 14 illustrates the new.html.erb view. The functionality is very raw. At this point, you can create Dinners. However, the Dinners Controller does not provide a way to associate RSVPs to Dinners. But don’t worry, you’ll get there!
Reviewing what Rails Provided
The database for this demo is SQLite 3. The database is located in the db and is named development.SQLite3. There is a FireFox add-in that you can download that allows you to open and view a SQLite database. You can find the manager here: https://addons.mozilla.org/en-US/firefox/addon/5817. Once installed, you can access the manager from the FireFox Tools Menu.
Figure 15 illustrates how the FireFox SQLite db manager works. Looking at the RSVP table, you can see that Rails did a lot of work for you. Notice the Dinner_id field. This is the foreign key to the Dinners table. Because you told Rails that RSVP references Dinner, Rails took care of adding the required field to facilitate that relationship. Notice too that Rails created two audit columns to capture the create and update datetime values for each RSVP record.
Looking at Figure 14 again, notice that for date time fields, Rails went ahead and provided a series of dropdowns to assist with data entry. In the next section, I’ll show you how to substantially overhaul what Rails provided in order to get the same look, feel and behavior of the original ASP.NET MVC Nerd Dinner application.
Before continuing, I encourage you to review the controllers and views. With respect to views, Rails and ASP delimits script the same way: <% %>. With respect to controllers, the first thing you will notice is that there is not a lot of code. There are many Ruby on Rails books and resources available that will get you up and running very quickly. A great place to start is to check out the Ruby on Rails site: http://guides.rubyonrails.org/getting_started.html. From there, you can find any number of resources.
Nerd Dinner on Rails - Getting to the Finish Line
Let’s cut to the chase. Looking at Figure 16 and Figure 17, the Rails version of Nerd Dinner is, feature for feature, nearly identical to the original ASP.NET MVC version. Of course, I couldn’t resist adding a few features that are not in the original Nerd Dinner application. One feature I did add was to set limits on the search radius when looking for a Dinner. In Figure 17, you can see where the search distance default is 100 miles. To accomplish this feature, I used the GeoKit Gem. I’ll discuss the details of that code later in this article.
At the start of this project I identified the major features Nerd Dinner on Rails had to support:
- User authentication
- Create/Edit/Delete Dinners
- RSVP to a Dinner
- Search for Dinners based on a specified location and distance threshold
- Ability to display search results with respective distance from search location
- Full map support like the original Nerd Dinner ASP.NET MVC application
- Retain the overall look and feel of the original ASP.NET MVC Nerd Dinner application
- Full Ajax, JSON and jQuery support
At the outset of this article, I said that Nerd Dinner is not defined so much by the business problem it solves. Rather, it is defined by the simple way it illustrates the flexibility and power of the ASP.NET MVC framework. In addition to the core ASP.NET MVC framework, Nerd Dinner also demonstrates how to use Ajax, JSON and jQuery. Ajax, JSON and jQuery are first-class citizens in Rails. As in ASP.NET MVC, you can use Ajax in Rails to make calls to the server without requiring an entire page refresh. JSON, which stands for JavaScript Object Notation, allows you to serialize data from a controller method to the browser. jQuery, another powerful JavaScript library, fully supports Ajax and JSON and facilitates the passing of data to/from the server and the display of data in the browser.
In the MVC context, Ajax is typically used to either render a string or a view back to a target <div> region. JSON is about data serialization to/from the server to the client. In the Nerd Dinner application, JSON is used to display Dinner search results from the server to the client. Ajax is used as part of the RSVP process. Looking at Ajax and JSON alone, Nerd Dinner is a great teaching tool.
Nerd Dinner on Rails - Under the Covers
Like the original Nerd Dinner on ASP.NET MVC, Nerd Dinner on Rails is based on the Model View Controller (MVC) pattern. At this point, you have already seen the basic Rails framework and the database. The base Rails application as compared to the original Nerd Dinner ASP.NET MVC application is a lump of clay. At a raw level, it has rudimentary functionality. What it lacks is the look and feel and full functionality of the original Nerd Dinner application. In the following sections, that raw lump of clay will be transformed into a fully functional and complete Nerd Dinner on Rails.
User Authentication
There is still one piece of unfinished business with respect to user authentication. In the original version, in order to host and RSVP to a Dinner, you must be logged into the system. The ASP.NET MVC version uses ASP Forms Authentication. In the Rails version, I used the Restful Authentication plug-in.
The first step is to install the plug-in:
ruby script/plugin install http://svn.techno-
weenie.net/projects/plugins/restful_authentication
Figure 18 illustrates the console output once the plug-in is installed. The next step is to generate the user model, view and controller and db scripts:
ruby script/generate authenticated user sessions
Like the plug-in installation, the script generation will result in console output that enumerates what has been generated.
The last step is to run the Rake migration script:
Rake db:migrate
There are also a few additions required to the routes.rb file. The routes.rb performs the same function as the MVCApplication Class RegsiterRoutes method in the Global.asax file. Regardless of which MVC framework you use, the route concept applies. The routes.rb file is located in the config folder. The config folder also hosts a number of files that handle configuration-related tasks for the database and the overall environment.
Dinner: Model, View and Controller
Ultimately, the best way to learn a new language and a development environment is to simply work through code to learn syntax and to create small programs to learn the framework. Gradually, you build up the complexity of your applications as you learn more. When possible, you should try and take something you know in one environment and replicate it in another environment. That is precisely what I did with Nerd Dinner. Like the ASP.NET MVC version, the Rails version has models, views and controllers. As you will see, while the implementation differs, conceptually, the ASP.NET MVC and Rails versions are identical.
Dinner Model
In Rails, models are very simple. Models are based on ActiveRecord, the Rails ORM mechanism. As you can see in Listing 1, there are no defined properties. Instead, attributes are dynamically read out of the database.
Even if you have never looked at Ruby code before, you will probably have a good sense of what the code in Listing 1 does:
- It’s a model class named Dinner.
- The model has a related model called RSVP (upon destruction of the Dinner, the RSVPs should also be destroyed.
- Eventdate, Title, Description, Contactphone and Address are required data.
- There is a custom method called Formatted_Eventdate that returns a formatted date string. Dates in this system are stored in UTC format.
- There are methods to determine whether a specific user hosts the Dinner or whether that user has already RSVP’d the Dinner.
In the IsUserRegistered method, you can see that models, which are based on ActiveRecord, have methods that are reminiscent of LINQ. In this method, RSVPs are searched using the current Dinner and passed attendee name as criteria. In the Dinner show view, you will see how these methods are called.
Dinner View (Edit, New, Show & Index)
If you understand how views work in ASP.NET MVC, you already understand how views work in Rails. In both environments, you have different views for different actions. Dinners supports the following actions:
-
Index Default home page for Dinners
-
Show Displays detail about a selected Dinner
-
New Called when the user wishes to create a Dinner - Get
-
Create Called when the user wishes to save a new Dinner - Post
-
Edit Called when the user edits the selected Dinner - Get
-
Update Called when the user saves edits - Post
-
rsvp Action invoked via Ajax when a user wishes to RSVP to the selected Dinner
-
Destroy. Called when the user confirms the selected Dinner should be deleted
Before you continue, you need to address the look and feel of Nerd Dinner. In the original version, the Site.css style sheet file determined the look and feel. You can find that file under the Content Folder. In Rails the process is no different. Under the public folder, there is a stylesheets folder. At the same level as stylesheets, there are also folders called javascripts and images. You just need to copy the respective resources to the correct folders:
- site.css => public\stylesheets
- map.js => public\stylesheets
- nerd.jpg => public\stylesheets
Copying the files is not enough. You need to add code to the views in order to take advantage of these resources. In ASP.NET MVC you can base views off of site.master. You can find site.master in your views\shared folder. The same concept exists in Rails. Under the views folder you’ll find a folder called layouts. If you want all of your views to be based off of a common template, you do so by creating a file called application.html.erb. Listing 2 shows a common layout (functional equivalent of site.master) for Nerd Dinner on Rails.
This code is very similar if not nearly identical to an asp page. It contains a mixture of script and HTML. Like ASP.NET MVC, Rails has a number of helper methods for tasks such as creating links. The application layout page specifies a common layout. Notice the helper methods that Rails provides to include css and script files. The prototype.js file, included with Rails, provides Ajax support. You’ll see how this is implemented in the RSVP action.
With the default layout defined, take a look at the show view (Listing 3).
Reading the code in Listing 3, it is nearly the same as the ASP code. It has a div to display basic information about the Dinner and a special div for the map display. In addition, there is code to determine if the current user is also the host or whether the current user has already RSVP’d. Depending on the results of those evaluations, the appropriate text is displayed. Pay special attention the rsvpmsg div, and this code in particular:
<%= link_to_remote( "RSVP to this dinner",
:update => "rsvpmsg",
:url =>{ :action => :rsvp,
:id => @dinner.id,
:user => current_user.login},
:success => "AnimateRSVPMessage()") %>
The link_to_remote helper is how Ajax calls are made in Rails. If you recall, the dinners controller has an action called rsvp. I’ll show you that code in a moment. That action does not return a view. Instead, the rsvp action returns a string. The :update directive specifies which div is to receive the results of Ajax call. In this case, the rsvpmsg div is updated. Upon a successful call, the AnimateRSVPMessage JavaScript function is called.
In the ASP.NET MVC version, this same code exists in the rsvpstatus.ascx partial view:
<%= Ajax.ActionLink("RSVP for this dinner",
"Register", "RSVP", new { id = Model.DinnerID
}, new AjaxOptions { UpdateTargetId =
"rsvpmsg", OnSuccess =
"AnimateRSVPMessage" })%>
Throughout the view you can see how ActiveRecord exposes the Dinner Model. In ASP.NET MVC, you can determine whether a view is strongly typed. From a controller action, you can easily pass an object to the view. From within the view you can access that same object via the Model Object. Rails handles all of this for you behind the scenes. The context, in this case, is the Dinner model. Therefore, from the view, you can simply refer to the Dinner instance variable: @dinner.
ActiveRecord is a true ORM. Notice how you can loop through the RSVPs of a Dinner:
<%@dinner.Rsvp.each do |rsvp| %>
<li>
<%if rsvp.Attendeename == current_user.login%>
<span style="color: red">You</span>
<%else%>
<%=h rsvp.Attendeename%>
<%end%>
</li>
<%end%>
Take a moment and examine the other views. Like this view, they inherit from the application layout and have full access to the data via the @dinner instance variable.
Dinners Controller
The last major code block to review is the Dinners Controller.
The Dinners Controller Rails works in the same way as the ASP.NET MVC version. In the MVC pattern, the controller processes requests from the view. Remember I discussed the various dinner actions in the previous section. The scaffolding process generated much of this code. To illustrate how things operate, let’s take a look at the index method:
def index
@dinners =
Dinner.find(:all,
:conditions => ['Eventdate >=
?',DateTime.now])
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @dinners }
end
end
For the index view, you only want future Dinners. Again, relying on ActiveRecord’s native features, the find method with conditions is used to locate Dinners. Notice in the response there are two options. One is html. In that case, the index.html.erb view will be rendered. You can see the output for that in Figure 19.
What if the url was altered to: http://localhost:3000/dinners.xml instead? Figure 20 shows what is rendered in that case.
Notice the first line of code after the class declaration:
before_filter :login_required
This is how authentication is handled. ASP.NET MVC works much the same way. In ASP.NET MVC, the [Authorize] attribute is used to determine whether certain functionality requires a user to be logged into the system.
The rest of the Dinner controller actions operate in the same way. The one exception is the rsvp action, which as previously mentioned supports the rsvp Ajax call.
JSON Support
Before wrapping things up, let me briefly talk about how Rails supports JSON. JSON stands for JavaScript Object Notation. JSON provides a means to transfer data to/from a controller action from a JavaScript function. In Nerd Dinner, JSON is used to support the Dinner search function. On the main page you can search for nearby Dinners based on a zip code or an address. In the Rails version of Nerd Dinner, I added the ability to specify the search radius. Figure 21 illustrates the search capabilities in action.
The resulting list that appears to the right of the map is data served up by the Search Controller’s SearchByLocation method. Listing 5 show the search code.
The search functionality is quite simple. For the geographical calculations I used the Geokit gem, which I discussed earlier. Once all of the future Dinners have been retrieved, the code loops through the data and uses GeoKit to determine if the Dinner is within the search boundary. If it is, it is added to the @jsondinners array. @jsondinners is simply an array of JsonDinner objects. Listing 6 shows the class definition for JsonDinner:
This code is nearly identical to the ASP.NET MVC version. I modified the class slightly to include the distance. Figure 21 illustrates how the distance, calculated by Geokit, is referenced in the list of found Dinners.
So how does the search mechanism work? If there is a complicated aspect to Nerd Dinner, this is it. But once you go through the code a few times, it all makes sense. In map.js (in public\javascripts), there is a JavaScript function called FindDinnersGivenLocation:
function FindDinnersGivenLocation(where) {
map.Find("", where, null, null,
null, null, null, false,
null, null, callbackUpdateMapDinners);
}
All this function does is take the data entered into the search textbox and centers the map on that location. In the map’s Find method you can specify a callback function called callbackUpdateMapDinners (Listing 7) that is to be called upon completion of the Find method.
While there is a lot of code here, what is happening is actually quite simple. Once the map has completed its Find method call (which was initiated by the search button), a post is made to the SearchContoller/SearchByLocation method. In that post, four parameters are passed:
-
Latitude Map center
-
Longitude Map center
-
Location The text the user entered
-
Distance The distance threshold the user specified
Those four pieces of data are then used by the SearchByLocaton method which I have already covered. If you recall, the SearchByLocation method renders an array of JsonDinner objects. The next bit of JavaScript code acts on that datastream that was returned by the SearchByLocation method:
function(dinners) {
$j.each(dinners, function(i, dinner)
For each item that was returned by the SearchByLocation method, a map point is created and the map point is loaded with data that is displayed when the user hovers the mouse over the point. In addition, a list of found Dinners is created to the right of the map.
That’s it!
This is why Nerd Dinner is a deceptively simple application. The business problem is not complicated. What Nerd Dinner does well is that within the context of a small business application, it demonstrates the power of the ASP.NET MVC framework. Further, the Nerd Dinner shows you how to incorporate jQuery. JavaScript, Ajax and JSON into your applications.
Rails Testing
At the beginning of this article I discussed how testing is a first-class citizen of the Rails framework. In Rails development it is such a core principle that in the case of gems, if your submission does not have a complete battery of tests, you need not bother submitting the gem! So then, how do you write a unit test in Rails? It is actually quite simple.
The Nerd Dinner Rails project has a folder named test. The test folder hosts the test fixtures and the code for, among other things, functional, integration and unit tests. Fixtures, like some configuration files, have a yml extension. Pronounced YAMAL, yml files (http://en.wikipedia.org/wiki/YAML) is another way to serialize data.
A test fixture hosts test data that is inserted into the test database prior to the running of a unit test. Like testing frameworks such as NUnit and MBUnit, the Rails environment takes care of database setup and teardown. Rails, through the data in the fixture files, puts the test database in a known good state prior to the running of each test (Listing 8).
Under the test\unit directory you will find a dinner_test.rb file. This file hosts the dinner related unit tests. Listing 9 illustrates the contents of this file.
Like NUnit and MBUnit, unit testing in Rails is based on assertions. Based on a pre-defined data condition, there should only be one dinner and that dinner will have a specific title. Listing 10 illustrates how to invoke the test.
Resources
The Ruby and Rails communities are rich and vibrant and you’ll find plenty of resources available to get you started. Here are a few of my favorite Ruby and Rails resources:
-
RailsCasts.com Free Ruby and Rails screen casts.
-
yehudakatz.com Yehuda is a member of the core Rails team and is also a contributor to the jQuery Project.
-
RubyInside.com
-
Programming Ruby 1.9 (PickAxe)
-
Edgecase Ruby Koans http://github.com/edgecase/ruby_koans
-
Agile Web Development with Rails (Pragmatic Bookshelf)
-
Rails Envy podcast, <a href="http://itunes.apple.com/us/podcast/rails-envy-podcast/id265693109">http://itunes.apple.com/us/podcast/rails-envy-podcast/id265693109</a>
Most of these many resources are free. If you live in large metro area, chances are good there is a Ruby/Rails user group near you.
Speaking of cost, everything shown in this article is free!
Summary
Now that I have a handle on what Rails can do, within the context of this exercise, the next step for me is to learn how to create a Rails application from the ground up using a TDD/BDD approach. Rails 3 is right around the corner and represents a big departure from the current version. I am very intrigued by what R:Spec/Cucumber has to offer. This was my first go around with Ruby on Rails and I am thoroughly impressed. The Ruby Language is very powerful and very intuitive. I look forward to what Rails 3.0 has to offer!
Listing 1: Dinner model
class Dinner < ActiveRecord::Base
has_many :Rsvp, :dependent => :destroy
validates_presence_of :Eventdate,
:Title,
:Description,
:Contactphone,
:Address
def Formatted_Eventdate
if self.Eventdate != nil
self.Eventdate.to_s(:event_date)
end
end
def Formatted_Eventdate=(value)
self.Eventdate = Time.parse(value)
end
def IsHostedBy(user,id)
@dinner = Dinner.find(:first,
:conditions=>['Hostedby == ? and id==?',user,id])
return @dinner
end
def IsUserRegistered(user,id)
@rsvp =
Rsvp.find(:first, :conditions =>['Attendeename==? And
Dinner_id==?',user,id])
return @rsvp
end
end
Listing 2: application.html.erb (site.master in ASP.NET MVC)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"<a href="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd</a>">
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>" xml:lang="en" lang="en">
<head id="Head1">
<%= javascript_include_tag "prototype" %>
<%= stylesheet_link_tag 'Site' %>
<script src=
"<a href="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2">http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2</a>"
type="text/javascript">
</script>
<script src="/javascripts/<a href="http://jquery-1.2.6.min.js">jquery-1.2.6.min.js</a>"
type="text/javascript">
</script>
<script src="/javascripts/Map.js" type="text/javascript">
</script>
<script src="/javascripts/<a href="http://jquery-1.2.6.min.js">jquery-1.2.6.min.js</a>"
type="text/javascript">
</script>
<script src="/javascripts/<a href="http://jquery-ui-1.7.2.custom.min.js">jquery-ui-1.7.2.custom.min.js</a>"
type="text/javascript" >
</script>
<meta http-equiv="content-type"
content="text/html;charset=UTF-8" />
</head>
<body>
<div class="page">
<div id="header">
<div id="title">
<table>
<tr>
<td><h1>NerdDinner</h1></td>
<td><h1>On</h1></td>
<td><img src="/Images/rails.png" align="left"
height="100" /></td>
</tr>
</table>
</div>
<div id="logindisplay">
<% if logged_in? %>
Welcome <strong><%=h current_user.login %></strong>!
<%= link_to '[ Log Off ]', logout_path %>
<% else %>
<%= link_to '[ Log On ]', login_path %>
<% end %>
</div>
<div id="menucontainer">
<ul id="menu">
<li><%= link_to 'Find a Dinner', home_path %></li>
<li><%= link_to 'Host a Dinner', create_path %></li>
<li><%= link_to 'About', about_path %></li>
</ul>
</div>
</div>
<div id="main">
<%= yield %>
</div>
</div>
</body>
</html>
Listing 3: Dinner Show View (dinners/show/{id}
<div id="dinnerDiv">
<h2><%=h @dinner.Title %></h2>
<p><strong>When:</strong>
<%=h @dinner.Formatted_Eventdate%></p>
<p><strong>Where:</strong>
<%=h @dinner.Address %>, <%=h @dinner.Country %></p>
<p><strong>Description:</strong> <%=h @dinner.Description %></p>
<p><strong>Organizer:</strong>
<%=h @dinner.Hostedby %> (<%=h @dinner.Contactphone %>)</p>
<input type="hidden" id="dinner_Latitude"
value = "<%=h @dinner.Latitude %>" />
<input type="hidden" id="dinner_Longitude"
value = "<%=h @dinner.Longitude %>" />
<div id="rsvpmsg">
<%if
@dinner.IsHostedBy(current_user.login, @dinner.id)
== nil%>
<%if
@dinner.IsUserRegistered(current_user.login, @dinner.id)
== nil%>
<%= link_to_remote( "RSVP to this dinner", :update =>
"rsvpmsg", :url =>{ :action => :rsvp,
:id => @dinner.id, :user => current_user.login},
:success => "AnimateRSVPMessage()") %>
<%end%>
<%end%>
</div>
<div id="rsvp_host_status">
<strong>
<%if @dinner.IsHostedBy(current_user.login, @dinner.id) !=
nil%>
<p>You are hosting this dinner!</p>
<%else%>
<%if @dinner.IsUserRegistered(current_user.login,
@dinner.id) != nil%>
<p>You have already RSVP'd to this dinner!</p>
<%end%>
<%end%>
</strong>
</div>
<%if @dinner.Rsvp.count > 0%>
<br/>
<div id="attendees">
<strong>Attendees:</strong>
<ul>
<%@dinner.Rsvp.each do |rsvp| %>
<li>
<%if rsvp.Attendeename == current_user.login%>
<span style="color: red">You</span>
<%else%>
<%=h rsvp.Attendeename%>
<%end%>
</li>
<%end%>
</ul>
</div>
<%end%>
<%if current_user.login == @dinner.Hostedby %>
<%= link_to 'Edit Dinner', edit_dinner_path(@dinner) %>
<%= link_to 'Delete Dinner',
@dinner, :confirm => 'Are you sure?',
:method => :delete %>
<%end%>
</div>
<div id="mapDiv">
<div id="theMap" style="width:520px; z-index:0;">
</div>
</div>
<script type="text/javascript">
var $j = jQuery.noConflict();
function AnimateRSVPMessage() {
$j("#rsvpmsg").animate({ fontSize: "1.5em" }, 400);
}
$j(document).ready(function() {
var latitude = $j("#dinner_Latitude").val();
var longitude = $j("#dinner_Longitude").val();
if ((latitude == 0) || (longitude == 0))
LoadMap();
else
LoadMap(latitude, longitude, mapLoaded);
});
function mapLoaded() {
var title = "<%= @dinner.Title %>";
var address = "<%=h @dinner.Address %>";
LoadPin(center, title, address);
map.SetZoomLevel(14);
}
</script>
Listing 4: DinnersController
class DinnersController < ApplicationController
before_filter :login_required
def index
@dinners =
Dinner.find(:all,
:conditions => ['Eventdate >= ?',DateTime.now])
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @dinners }
end
end
def show
@dinner = Dinner.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @dinner }
end
end
def new
@dinner = Dinner.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @dinner }
end
end
def edit
@dinner = Dinner.find(params[:id])
if !@dinner.IsHostedBy(current_user.login, @dinner.id)
redirect_to :action => "show"
end
end
def rsvp
@msg = ""
@dinner = Dinner.find(params[:id])
@rsvp = @dinner.IsUserRegistered(params[:user],@dinner.id)
if @rsvp == nil
@rsvp = Rsvp.new(:Attendeename=>params[:user],
:Dinner_id=>@dinner.id)
if @rsvp.save
@msg = "<H2>Thanks - we'll see you there!</H2>"
else
@msg = "<H2>There was a problem with your RSVP.
Please try again.</H2>"
end
end
render :text => @msg
end
def create
@dinner = Dinner.new(params[:dinner])
respond_to do |format|
if @dinner.save
flash[:notice] = 'Dinner was successfully created.'
format.html { redirect_to(@dinner) }
format.xml { render :xml => @dinner,
:status => :created, :location => @dinner }
else
format.html { render :action => "new" }
format.xml { render :xml => @dinner.errors, :status =>
:unprocessable_entity }
end
end
end
def update
@dinner = Dinner.find(params[:id])
respond_to do |format|
if @dinner.IsHostedBy(current_user.login, @dinner.id)
if @dinner.update_attributes(params[:dinner])
flash[:notice] = 'Dinner was successfully updated.'
format.html { redirect_to(@dinner) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml =>
@dinner.errors, :status => :unprocessable_entity }
end
end
end
end
def destroy
@dinner = Dinner.find(params[:id])
@msg = "Your selected dinner [" + @dinner.Title + "] has been
deleted."
if @dinner.IsHostedBy(current_user.login, @dinner.id)
@dinner.destroy
flash[:notice] = @msg
else
flash[:notice] = "You must be the organizer
of this dinner in order to delete/edit."
end
respond_to do |format|
format.html { redirect_to(dinners_url) }
format.xml { head :ok }
end
end
end
Listing 5: SearchByLocation method
class SearchController < ApplicationController
require 'jsondinner.rb'
def SearchByLocation
@latitude = params[:latitude]
@longitude = params[:longitude]
@location = params[:location]
@distance = params[:distance]
@origin = Geokit::Geocoders::YahooGeocoder.geocode(@location)
@dinners = Dinner.find(:all,
:conditions=> ["Eventdate > ?",Time.now.utc])
@jsondinners = []
@dinners.each do |dinner|
@target = Geokit::Geocoders::
YahooGeocoder.geocode(dinner.Address)
@distance_to = @origin.distance_to(@target)
if @distance_to <= @distance.to_i
@jsondinner =
JsonDinner.new(dinner.id,dinner.Title,
dinner.Latitude,dinner.Longitude,
dinner.Description,
dinner.Rsvp.count,@latitude,@longitude,
@location,@distance_to)
@jsondinners.push(@jsondinner)
end
end
if @jsondinners.length == 0
@jsondinner =
JsonDinner.new(0,"No dinners matched your
criteria",0,0,"",0,0,0,0,0)
@jsondinners.push(@jsondinner)
end
@jsondinners.sort! {|a,b| a.Distance <=> b.Distance}
respond_to do |format|
format.json { render :json => @jsondinners }
end
end
end
Listing 6: JsonDinner class definition
class JsonDinner
def initialize(dinnerid,title,latitude,longitude,
description,rsvpcount,searchlatitude,searchlongitude,
searchlocation,distance)
@DinnerID = dinnerid
@Title = title
@Latitude = latitude
@Longitude = longitude
@Description = description
@RSVPCount = rsvpcount
@SearchLatitude = searchlatitude
@SearchLongitude = searchlongitude
@SearchLocation = searchlocation
@Distance = distance
end
end
Listing 7: callbackUpdateMapDinners function in map.js
function callbackUpdateMapDinners(layer, resultsArray, places,
hasMore, VEErrorMessage)
{
$j("#dinnerList").empty();
clearMap();
var center = map.GetCenter();
$j('#latitude').val(center.Latitude)
$j('#longitude').val(center.Longitude)
$j.post("/Search/SearchByLocation",
{ latitude: center.Latitude,
longitude: center.Longitude,
location: $j("#Location").val(),
distance: $j("#Distance").val()
},
function(dinners) {
$j.each(dinners, function(i, dinner)
{
//Add a dinner to the <ul> dinnerList on the right
if (dinner.DinnerID > 0) {
var LL = new VELatLong(dinner.Latitude,
dinner.Longitude, 0, null);
var RsvpMessage = "";
if (dinner.RSVPCount == 1)
RsvpMessage = "" + dinner.RSVPCount + " RSVP";
else
RsvpMessage = "" + dinner.RSVPCount + " RSVPs";
// Add Pin to Map
LoadPin(LL, '<a href="/dinners/show/' +
dinner.DinnerID + '">'
+ dinner.Title + '</a>',
"<p>" + dinner.Description + "</p>" +
RsvpMessage);
$j('#dinnerList').append($('<li/>')
.attr("class", "dinnerItem")
.append($('<a/>').attr("href",
"/dinners/show/" + dinner.DinnerID)
.html(dinner.Title)).append(" (" + RsvpMessage +
" - (" +
Math.round(dinner.Distance*Math.pow(10,2))
/
Math.pow(10,2) + " mi)"+")"));
}
else
{
$j('#dinnerList').append($('<li/>')
.attr("class", "dinnerItem")
.append($('<a/>').attr("href",
"/create/")
.html(dinner.Title)));
}
});
// Adjust zoom to display all the pins we just added.
if (points.length > 1) {
map.SetMapView(points);
// Display the event's pin-bubble on hover.
$j(".dinnerItem").each(function(i, dinner) {
$(dinner).hover(
function() { map.ShowInfoBox(shapes[i]); },
function() { map.HideInfoBox(shapes[i]); }
);
});
}
}, "json");
}
Listing 8: dinners.yml Test fixture
one:
id: 1
Title: Test Dinner
Eventdate: <%= 5.days.from_now.to_s :db %>
Description: Test Dinner
Hostedby: quentin
Contactphone: 610-555-1212
Address: 1234 Market Street Philadelphia, PA 19103
Country: USA
Latitude: 0
Longitude: 0
Listing 9: dinner_test.rb Dinner unit test class
require File.dirname(__FILE__) + '/../test_helper'
class DinnerTest < ActiveSupport::TestCase
def test_should_get_dinner
@dinner = Dinner.find(:all)
assert_equal 1, @dinner.length
assert_equal 'Test Dinner',@dinner[0].Title
end
end
Listing 10: Unit Test Output
C:\dev\Nerd-Dinner-on-Rails>ruby test\unit\dinner_test.rb
Loaded suite test/unit/dinner_test
Started
.
Finished in 1.086 seconds.
1 tests, 2 assertions, 0 failures, 0 errors