[MVC]Kigg框架

Kigg – Building a Digg Clone with ASP.NET MVC Part – 1

Published: 20 Feb 2008
By: Kazi Manzur Rashid
Download Sample Code

Learn how to develop a Digg-like application with ASP.NET MVC, LINQ to SQL and ASP.NET AJAX.

Introduction

For the last few days, I have been trying to get my hands dirty with the new ASP.NET MVC framework. I saw many discussions on some of the advanced topics like IoC Container/DI, View Engine, Controller factory and so on, but I could not find a simple application to harness the power of the new ASP.NET MVC framework. Certainly, knowing these things is an added benefit but it is not mandatory to develop applications with the ASP.NET MVC Framework. In this article – from the DotNetSlackers team – I will present a basic version of Digg / DotNetKicks kind of application developed with the ASP.NET MVC framework. You will find the whole application running at the following link:

[Live Demo]

Note: The article and code are based on the first preview release of the ASP.NET 3.5 Extensions. We will be updating it as per the new release.

Prerequisites

A brief Introduction to the ASP.NET MVC Framework by Scott Guthrie:

An excellent Screencast by Scott Hanselman.

Overview

The MVC (Model-View-Controller) is a popular pattern to develop UI-centric applications based on a simple concept: divide the implementation into three logical components:

  • Model,
  • View,
  • Controller.

The ASP.NET MVC Framework is an implementation of the MVC pattern, and has built-in support for developing web applications. Let's take a quick look at these three components.

Figure 1: The MVC Framework

images/mvc.jpg

  • Model: Refers to the domain logic of your application. Usually the state of the Model is persisted in the database. Consider it as the middle tier in a typical n-Tier application, which consist of the business logic and domain objects.
  • View: Is typically the User Interface, which is used to show the Model data and take input from the User.
  • Controller: Is responsible for handling the user interaction. This is the Ultimate Driver of the whole ASP.NET MVC Framework. Depending upon the User gesture it decides which action to perform on the model; it also builds the view data and finally decides which View to render.

The ASP.NET MVC Framework is an alternate and better way to develop web applications, comparing to the regular web form model. It provides us with:

  • Clean Separation of Concerns, where each component is serving only one purpose. Thus it also gives us the opportunity to integrate TDD (Test-Driven Development) in the development workflow and unit test each component without considering the others, as most of the framework components are interface-based. This allows us to mock them out.
  • The whole framework is very much extensible and pluggable. It is really easy to replace or customize each part without affecting the others.
  • Pretty/SEO (Search Engine Optimization) URLs. Full controls of how URLs are constructed. No need to do URL Rewriting anymore.
  • True stateless model of the web. We no longer have to deal with postbacks and ViewState.
  • Full control of the generated HTML. This means no more naming containers.
  • Utilize the existing knowledge of ASP.NET like Providers, Caching, Configuration and so on.

Request Flow

In regular ASP.NET web form applications, URLs are usually mapped to physical disk files. When a URL is requested, the code of the associated file gets executed, which generates the HTML. However, in the ASP.NET MVC Framework the URLs are tied with the Controllers rather than the disk file. The component that is responsible to map the URL with the Controller is the Routing Handler. When the application starts, it is required to register the URL Routing Rules, which the Routing Handler uses to map the controller when the request arrives. Let's take a quick view of how a request is carried in different layers of the ASP.NET MVC Framework:

Figure 2: The Request Flow

images/request.jpg

  • The user requests a URL.
  • The ASP.NET MVC Framework evaluates the requested URL against the registered Routing Rules and finds a matching Controller. The Framework forwards the request to the matching Controller.
  • The Controller calls the Model to build ViewData. It can call the model multiple times depending upon the ViewData that it is building.
  • The Model, as mentioned earlier, is the middle tier, which might involve data access components, workflow activities, external web service dependencies etc. The Model returns the requested data to Controller.
  • The Controller selects a View and passes the data to the View which it previously got from the Model. The View Renders the data and returns the generated HTML to the User.

Default Conventions

There are few default conventions that we have to know before moving forward.

First, when matching the Controller for the incoming request, the framework uses a pattern UrlPathController to match the Controller. For example, if the request is for http://www.example.com/Home, it will match the HomeController for that request. Once the request reaches the Controller, the Controller executes a specified action from the URL sub-path, or it executes the default action if the action is missing in the URL. The default action for a Controller is specified when the URL Routing Rules are declared in the application start event handler. The actions are declared in the Controller class as methods. For example, if the request is for http://www.example.com/Home/Index, it will automatically execute the Index method of the HomeController; and if the URL contains more sub-paths, it will pass each sub-path as a parameter value of that method.

Next, when creating an ASP.NET MVC Project in Visual Studio, it automatically creates Controllers, Models and Views folders. It is recommended to create the Controllers, Models and Views in the corresponding folders. However, if you are developing a large application you can choose to implement the Model in one or more different projects, but the Controllers and Views must be present in the MVC Project. For each Controller, there should be a folder under the Views with the same name. For example, if we have a Controller named HomeController, there should be folder named Home under the Views folder. If a single view is used by more than one Controller, it should be placed under the Shared folder of the Views. The shared folder can also contain shared User Controls, Stylesheets, JavaScript files and so on.

Philosophy and Functionalities of Kigg

Let us discuss the philosophy of Digg/DotNetKicks kinds of applications before moving to the implementation part. Both of these applications are completely community driven. Members find an interesting Internet resource, they submit it to the application and it appears instantly in the upcoming story queue. Other members can vote it, and once it reaches a certain number of votes, it appears in the front page.

The application will be able to:

  • List all the published Stories.
  • List the Stories by Category.
  • List the Upcoming Stories.
  • List Stories by Tag.
  • List Stories Posted By an Individual User.
  • Search Stories.
  • View the Details of a Story.
  • Allow user to Submit Story (Requires Login)
  • Allow user to Kigg (Vote) Story (Requires Login)
  • Allow user to post comment on a Story (Requires Login)
  • Allow the Users to Login.
  • Allow the users to Signup
  • Allow the User to reset lost password.

Controllers and Actions Defined

  • The functionalities of Kigg are Story and User related. Therefore, we can categorize these functionalities into two Controllers:
  • StoryController: Handles all Story Listing, Searching, Submitting, Kigging etc.
  • UserController: Handles Authentication, Signup, Forgot Password etc.

When evaluating the Controller Actions, a good technique is to give the action a name similar to its functionality. The following code shows the action methods of the StoryController:

  1. public class StoryController  
  2. {  
  3.     //List published stories for all category or for a specific category  
  4.     [ControllerAction]  
  5.     public void Category(string name, int? page)  
  6.     {  
  7.     }  
  8.   
  9.     //List all upcoming stories regardless the category  
  10.     [ControllerAction]  
  11.     public void Upcoming(int? page)  
  12.     {  
  13.     }  
  14.   
  15.     //List Stories for a specific tag  
  16.     [ControllerAction]  
  17.     public void Tag(string name, int? page)  
  18.     {  
  19.     }  
  20.   
  21.     //List Stories Posted by a Specific User  
  22.     [ControllerAction]  
  23.     public void PostedBy(string name, int? page)  
  24.     {  
  25.     }  
  26.   
  27.     //Search the Stories  
  28.     [ControllerAction]  
  29.     public void Search(string q, int? page)  
  30.     {  
  31.     }  
  32.   
  33.     //View the details of a specific story  
  34.     [ControllerAction]  
  35.     public void Detail(int id)  
  36.     {  
  37.     }  
  38.   
  39.     //Submit a Story  
  40.     [ControllerAction]  
  41.     public void Submit(string storyUrl, string storyTitle, int storyCategoryId,   
  42.                        string storyDescription, string storyTags)  
  43.     {  
  44.     }  
  45.   
  46.     //Kigg the Story  
  47.     [ControllerAction]  
  48.     public void Kigg(int storyId)  
  49.     {  
  50.     }  
  51.   
  52.     //Post a Comment  
  53.     [ControllerAction]  
  54.     public void Comment(int storyId, string commentContent)  
  55.     {  
  56.     }  
  57. }  

And the following code snippet shows the action methods of UserController:

  1. public class UserController  
  2. {  
  3.     // Login  
  4.     [ControllerAction]  
  5.     public void Login(string userName, string password, bool rememberMe)  
  6.     {  
  7.     }  
  8.   
  9.     //Logout  
  10.     [ControllerAction]  
  11.     public void Logout()  
  12.     {  
  13.     }  
  14.   
  15.     // Reset the current password and mail back the new password  
  16.     [ControllerAction]  
  17.     public void SendPassword(string email)  
  18.     {  
  19.     }  
  20.   
  21.     //User Registration  
  22.     [ControllerAction]  
  23.     public void Signup(string userName, string password, string email)  
  24.     {  
  25.     }  
  26. }  

Note that the Action methods are declared as public and Marked with the ControllerAction attribute. In future versions of ASP.NET MVC the attribute will no longer be needed. Publicly declared method will automatically become the action methods.

Routing Rules Defined

Once the signatures of the Controllers are defined, it is time to declare the URL Routing Rules that will map the URLs to these Controllers' action methods. As mentioned earlier, the Routing Rules must be registered in the application start event handler, in the Global.asax file. When defining the Routing Rules, one thing you have to remember is keeping the most specific rule at the top. It is similar to the try/catch block rule of exception handling, where we put the more general exception at the bottom and specific exceptions at the top. If you open the Global.asax file of this application, you will find that we have explicitly created two methods to register these rules and call these methods in the application start event. The reasons behind creating these methods – instead of registering them in the application start event – are that we do not want to clutter our rules with both newer and older versions of IIS. When hosting an MVC application in IIS7 the URLs are extension-less, which is really cool, but in older versions of IIS the URLs are served with a .mvc extension. So to support both older and newer versions of IIS we need to add the same URL, one with the extension for the older version and another without extension for the newer version of IIS. Here, we are checking the web.config for the hosting IIS version and registering only the rules for that version. Another benefit of having explicit methods for registering the rules is Unit Testing, which we will check in a few moments. The following code shows the Routing Rules Declarations:

  1. protected void Application_Start(object sender, EventArgs e)  
  2. {  
  3.     RegisterRoutes(RouteTable.Routes);  
  4. } public static void RegisterRoutes(RouteCollection routes)  
  5. {  
  6.     int iisVersion = Convert.ToInt32(ConfigurationManager.AppSettings["IISVersion"]);  
  7.   
  8.     if (iisVersion >= 7)  
  9.     {  
  10.         RegisterRoutesForNewIIS(routes);  
  11.     }  
  12.     else  
  13.     {  
  14.         RegisterRoutesForOldIIS(routes);  
  15.     }  
  16. }   
  17. private static void RegisterRoutesForNewIIS(ICollection<Route> routes)  
  18. {  
  19.     var defaults = new  
  20.     {  
  21.         controller = "Story",  
  22.         action = "Category",  
  23.         name = (string)null,  
  24.         page = (int?)null  
  25.     };  
  26.   
  27.     routes.Add(  
  28.                     new Route  
  29.                     {  
  30.                         Url = "User/Login",  
  31.                         RouteHandler = typeof(MvcRouteHandler),  
  32.                         Defaults = new  
  33.                         {  
  34.                             controller = "User",  
  35.                             action = "Login"  
  36.                         }  
  37.                     }  
  38.                 );  
  39.   
  40.     routes.Add(  
  41.                     new Route  
  42.                     {  
  43.                         Url = "User/Logout",  
  44.                         RouteHandler = typeof(MvcRouteHandler),  
  45.                         Defaults = new  
  46.                         {  
  47.                             controller = "User",  
  48.                             action = "Logout"  
  49.                         }  
  50.                     }  
  51.                 );  
  52.   
  53.     routes.Add(  
  54.                     new Route  
  55.                     {  
  56.                         Url = "User/Signup",  
  57.                         RouteHandler = typeof(MvcRouteHandler),  
  58.                         Defaults = new  
  59.                         {  
  60.                             controller = "User",  
  61.                             action = "Signup"  
  62.                         }  
  63.                     }  
  64.                 );  
  65.   
  66.     routes.Add(  
  67.                     new Route  
  68.                     {  
  69.                         Url = "User/SendPassword",  
  70.                         RouteHandler = typeof(MvcRouteHandler),  
  71.                         Defaults = new  
  72.                         {  
  73.                             controller = "User",  
  74.                             action = "SendPassword"  
  75.                         }  
  76.                     }  
  77.                 );  
  78.   
  79.     routes.Add(  
  80.                     new Route  
  81.                     {  
  82.                         Url = "Story/Detail/[id]",  
  83.                         RouteHandler = typeof(MvcRouteHandler),  
  84.                         Defaults = new  
  85.                         {  
  86.                             controller = "Story",  
  87.                             action = "Detail"  
  88.                         }  
  89.                     }  
  90.                 );  
  91.   
  92.     routes.Add(  
  93.                     new Route  
  94.                     {  
  95.                         Url = "Story/Upcoming/[page]",  
  96.                         RouteHandler = typeof(MvcRouteHandler),  
  97.                         Defaults = new  
  98.                         {  
  99.                             controller = "Story",  
  100.                             action = "Upcoming"  
  101.                         }  
  102.                     }  
  103.                 );  
  104.   
  105.     routes.Add(  
  106.                     new Route  
  107.                     {  
  108.                         Url = "Story/Search/[q]/[page]",  
  109.                         RouteHandler = typeof(MvcRouteHandler),  
  110.                         Defaults = new  
  111.                         {  
  112.                             controller = "Story",  
  113.                             action = "Search"  
  114.                         }  
  115.                     }  
  116.                 );  
  117.   
  118.     routes.Add(  
  119.                     new Route  
  120.                     {  
  121.                         Url = "Story/Category/[page]",  
  122.                         RouteHandler = typeof(MvcRouteHandler),  
  123.                         Defaults = defaults  
  124.                     }  
  125.                 );  
  126.   
  127.     routes.Add(  
  128.                     new Route  
  129.                     {  
  130.                         Url = "Story/[action]/[name]/[page]",  
  131.                         RouteHandler = typeof(MvcRouteHandler),  
  132.                         Defaults = defaults  
  133.                     }  
  134.                 );  
  135.   
  136.     routes.Add(  
  137.                     new Route  
  138.   
  139.                     {  
  140.                         Url = "[controller]/[action]/[id]",  
  141.                         RouteHandler = typeof(MvcRouteHandler),  
  142.                         Defaults = defaults  
  143.                     }  
  144.                 );  
  145.   
  146.     routes.Add(  
  147.                     new Route  
  148.                     {  
  149.                         Url = "Default.aspx",  
  150.                         RouteHandler = typeof(MvcRouteHandler),  
  151.                         Defaults = defaults  
  152.                     }  
  153.                 );  
  154. }  

As you can see, we are registering the most specific rules like User/Login, User/Signup, Story/Detail, Story/Category first and least specific rules like Story/[action], [controller]/[action] later. When mentioning the variables in the URL format we are using [] to denote the variable. There are two fixed variable names in the MVC framework: [controller] and [action]. The others are simply the parameter names of the controller action methods. The last rule, which maps to default.aspx to the all category is required to handle the /.

Test the Routing Rules

Once the Routing Rules and Controller signatures are defined, it's time to test the Rules, which will give us a clear idea on whether the Rules are good enough to map the controller's action and if the correct parameter values are passed for a given URL. The following table shows the test bed of Routing rules that we like to test:

Table 1: Tests

Functionality Url Format Controller Action
Login User/Login UserController Login
SendPassword User/SendPassword UserController SendPassword
Signup User/Signup UserController Signup
Logout User/Logout UserController Logout
List All Published Story Story/Category
Story/Category/[page]
StoryController Category
List Published Stories for a specific Category Story/Category/[categoryName]
Story/Category/[categoryName]/[page]
StoryController Category
List Upcoming Stories Story/Upcoming
Story/Upcoming/[page]
StoryController Upcoming
List Stories for a specific Tag Story/Tag/[tagName]
Story/Tag/[tagName]/[page]
StoryController Tag
List Stories Posted By an User Story/PostedBy/[userName]
Story/PostedBy/[userName]/[page]
StoryController PostedBy
Search Stories Story/Search?q=query
Story/Search/[q]/[page]
StoryController Search
View Details of a Story Story/Detail/[storyID] StoryController Detail
Submit a Story Story/Submit StoryController Submit
Vote a Story Story/Kigg StoryController Kigg
Post a Comment Story/Comment StoryController Comment

You will find Route.cs in the test projects folder, which is used to test the Routes. We have included both VSTSTest and NUnit implementations of all the Unit Tests. For Mocking, we have used Rhino Mocks. The tests are done based upon the code that Phil Haack blogged few weeks ago in the post Testing Routes In ASP.NET MVC.

Here are few code snippets of the Routing Rule Test:

  1. [TestInitialize]  
  2. public void Init()  
  3. {  
  4.     routes = new RouteCollection();  
  5.     Global.RegisterRoutes(routes);  
  6.   
  7.     mocks = new MockRepository();  
  8. }  
  9.   
  10. [TestMethod]  
  11. public void VerifyDefault()  
  12. {  
  13.     IHttpContext httpContext;  
  14.   
  15.     using (mocks.Record())  
  16.     {  
  17.         httpContext = GetHttpContext(mocks, "~/Default.aspx");  
  18.     }  
  19.   
  20.     using (mocks.Playback())  
  21.     {  
  22.         RouteData routeData = routes.GetRouteData(httpContext);  
  23.   
  24.         Assert.IsNotNull(routeData);  
  25.         Assert.AreEqual("Story", routeData.Values["Controller"]);  
  26.         Assert.AreEqual("Category", routeData.Values["action"]);  
  27.     }  
  28. }  
  29.   
  30. [TestMethod]  
  31. public void VerifyAllCategory()  
  32. {  
  33.     IHttpContext httpContext;  
  34.   
  35.     using (mocks.Record())  
  36.     {  
  37.         httpContext = GetHttpContext(mocks, "~/Story/Category/20");  
  38.     }  
  39.   
  40.     using (mocks.Playback())  
  41.     {  
  42.         RouteData routeData = routes.GetRouteData(httpContext);  
  43.   
  44.         Assert.IsNotNull(routeData);  
  45.         Assert.AreEqual("Story", routeData.Values["Controller"]);  
  46.         Assert.AreEqual("Category", routeData.Values["action"]);  
  47.         Assert.IsNull(routeData.Values["name"]);  
  48.         Assert.AreEqual("20", routeData.Values["page"]);  
  49.     }  
  50. }  

Implementing the UserController

Earlier, we defined the signature of the UserController. Now it's time to add the actual implementation. The UserController uses the ASP.NET Membership provider for login, signup and similar operations. The only difference between this controller and the others is that all the methods of this controller returns JSON data instead of a regular HTML view. The controller methods are called from the client side using the ASP.NET AJAX Framework. The following code shows the Login method of this controller:

  1. [ControllerAction]  
  2. public void Login(string userName, string password, bool rememberMe)  
  3. {  
  4.     using (new CodeBenchmark())  
  5.     {  
  6.         JsonResult result = new JsonResult();  
  7.   
  8.         if (string.IsNullOrEmpty(userName))  
  9.         {  
  10.             result.errorMessage = "User name cannot be blank.";  
  11.         }  
  12.         else if (string.IsNullOrEmpty(password))  
  13.         {  
  14.             result.errorMessage = "Password cannot be blank.";  
  15.         }  
  16.         else if (!UserManager.ValidateUser(userName, password))  
  17.         {  
  18.             result.errorMessage = "Invalid login credentials.";  
  19.         }  
  20.         else  
  21.         {  
  22.             //The following check is required for TDD   
  23.             if (HttpContext != null)  
  24.             {  
  25.                 FormsAuthentication.SetAuthCookie(userName, rememberMe);  
  26.             }  
  27.   
  28.             result.isSuccessful = true;  
  29.         }  
  30.   
  31.         RenderView("Json", result);  
  32.     }  
  33. }  

As you can see, we are creating a JsonResult object at the beginning of the method. JsonResult is a simple class which has only two properties: isSuccessful and errorMessage, which denotes if the controller action was successful. If the operation is not successful, it populates the errorMessage with the proper reason. In the end, we are passing the result as view data of the shared view called "Json". Since this is a shared view and it will be used by both UserController and StoryController, we have placed it in the Shared folder under the Views. I am excluding the other methods of this controller as they work almost the same as this. One important thing I would like to mention is that instead of using the static Membership class directly in the code, we are passing the abstract membership provider in the constructor of this controller. The reason behind this is that we can pass a Mock Membership Provider in our Unit Test, which we will check next. And in the other constructor, we are passing the default membership provider, which is configured in the web.config.

Testing the UserController

For testing the Controller, we are also following the same test specific subclass pattern that Phil Haack blogged few weeks ago in the post Writing Unit Tests For Controller Actions. As mentioned in the previous section, we will pass a Mock Membership Provider to test the controller. We will expect that the proper method of the membership provider is called and that the controller is passing correct data to the view. The following code snippet shows how the Login is performed for a successful attempt and how it fails when an empty user name is provided.

  1. [TestInitialize]  
  2. public void Init()  
  3. {  
  4.     mocks = new MockRepository();  
  5.     userManager = mocks.PartialMock<MembershipProvider>();  
  6.     controller = new UserControllerForTest(userManager);  
  7. }  
  8.   
  9. [TestMethod]  
  10. public void ShouldLogin()  
  11. {  
  12.     using(mocks.Record())  
  13.     {  
  14.         Expect.Call(userManager.ValidateUser(DefaultUserName, DefaultPassword)).IgnoreArguments().Return(true);  
  15.     }  
  16.   
  17.     using(mocks.Playback())  
  18.     {  
  19.   
  20.         controller.Login(DefaultUserName, DefaultPassword, true);  
  21.     }  
  22.   
  23.     Assert.AreEqual(controller.SelectedView, "Json");  
  24.     Assert.IsInstanceOfType(controller.SelectedViewData, typeof(JsonResult));  
  25.     Assert.IsTrue(((JsonResult)controller.SelectedViewData).isSuccessful);  
  26.     Assert.IsNull(((JsonResult)controller.SelectedViewData).errorMessage);  
  27. }  
  28.   
  29. [TestMethod]  
  30. public void ShoudNotLoginForEmptyUserName()  
  31. {  
  32.     controller.Login(string.Empty, DefaultPassword, false);  
  33.   
  34.     Assert.AreEqual(controller.SelectedView, "Json");  
  35.     Assert.IsInstanceOfType(controller.SelectedViewData, typeof(JsonResult));  
  36.     Assert.IsFalse(((JsonResult)controller.SelectedViewData).isSuccessful);  
  37.     Assert.AreEqual(((JsonResult)controller.SelectedViewData).errorMessage, "User name cannot be blank.");  
  38. }  

Summary

My initial intention was to provide all the details in a single article, but as you can see, the article has already evolved too much to fit in a single part.

In this part, we took a brief overview of the ASP.NET MVC Framework and learned how to assign functionality to controllers, how to define the routing rules and test them against URLs. We have also seen how to create a Controller that returns JSON data instead of regular HTML, and tested the controller. In the next part, we will talk more about the Controller, which renders regular HTML view, using master pages and user controls to create the view, passing strongly typed view data to the view and finally creating the Model. Stay tuned.

About Kazi Manzur Rashid

Kazi Manzur Rashid, Nickname Amit is a Diehard fan of Microsoft Technology. He started programming with Visual Basic 5.0 back in 1996. Since then he has developed many diversified solutions in his professional career, which spans from Anti-Spyware Tool to Personalized Start Page. Currently He is wor…

View complete profile

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏