Yan-Feng

记录经历、收藏经典、分享经验

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Overview

The best way to learn a new framework is to build something with it. This first chapter walks through how to build a small, but complete, application using ASP.NET MVC, and introduces some of the core concepts behind it.

The application we are going to build is called "NerdDinner." NerdDinner provides an easy way for people to find and organize dinners online (Figure 1-1).

Image from book
Figure 1-1

NerdDinner enables registered users to create, edit and delete dinners. It enforces a consistent set of validation and business rules across the application (Figure 1-2).

Image from book
Figure 1-2

Chapter 1 is licensed under the terms of Creative Commons Attribution No Derivatives 3.0 license and may be redistributed according to those terms with the following attribution: "Chapter 1 "NerdDinner" from Professional ASP.NET MVC 1.0 written by Rob Conery, Scott Hanselman, Phil Haack, Scott Guthrie published by Wrox (ISBN: 978-0-470-38461-9) may be redistributed under the terms of Creative Commons Attribution No Derivatives 3.0 license. The original electronic copy is available at http://tinyurl.com/aspnetmvc. The complete book Professional ASP.NET MVC 1.0 is copyright 2009 by Wiley Publishing Inc and may not redistributed without permission."

 

Visitors to the site can search to find upcoming dinners being held near them (Figure 1-3):

Image from book
Figure 1-3
 

Clicking a dinner will take them to a details page where they can learn more about it (Figure 1-4):

Image from book
Figure 1-4

If they are interested in attending the dinner they can log in or register on the site (Figure 1-5):

Image from book
Figure 1-5

They can then easily RSVP to attend the event (Figures 1-6 and 1-7):

Image from book
Figure 1-6
Image from book
Figure 1-7
  

We are going to begin implementing the NerdDinner application by using the File ð New Project command within Visual Studio to create a brand new ASP.NET MVC project. We'll then incrementally add functionality and features. Along the way we'll cover how to create a database, build a model with business rule validations, implement data listing/details UI, provide CRUD (Create, Update, Delete) form entry support, implement efficient data paging, reuse the UI using master pages and partials, secure the application using authentication and authorization, use AJAX to deliver dynamic updates and interactive map support, and implement automated unit testing.

You can build your own copy of NerdDinner from scratch by completing each step we walk through in this chapter. Alternatively, you can download a completed version of the source code here: http://tinyurl.com/aspnetmvc.

You can use either Visual Studio 2008 or the free Visual Web Developer 2008 Express to build the application. You can use either SQL Server or the free SQL Server Express to host the database.

You can install ASP.NET MVC, Visual Web Developer 2008, and SQL Server Express using the Microsoft Web Platform Installer available at www.microsoft.com/web/downloads.

 

File ð New Project

We'll begin our NerdDinner application by selecting the File ð New Project menu item within Visual Studio 2008 or the free Visual Web Developer 2008 Express.

This will bring up the New Project dialog. To create a new ASP.NET MVC application, we'll select the Web node on the left side of the dialog and then choose the ASP.NET MVC Web Application project template on the right (Figure 1-8):

Image from book
Figure 1-8

We'll name the new project NerdDinner and then click the OK button to create it.

When we click OK, Visual Studio will bring up an additional dialog that prompts us to optionally create a unit test project for the new application as well (Figure 1-9). This unit test project enables us to create automated tests that verify the functionality and behavior of our application (something we'll cover later in this tutorial).

Image from book
Figure 1-9
 

The Test framework drop-down in Figure 1-9 is populated with all available ASP.NET MVC unit test project templates installed on the machine. Versions can be downloaded for NUnit, MBUnit, and XUnit. The built-in Visual Studio Unit Test Framework is also supported.

Note 

The Visual Studio Unit Test Framework is only available with Visual Studio 2008 Professional and higher versions). If you are using VS 2008 Standard Edition or Visual Web Developer 2008 Express, you will need to download and install the NUnit, MBUnit, or XUnit extensions for ASP.NET MVC in order for this dialog to be shown. The dialog will not display if there aren't any test frameworks installed.

We'll use the default NerdDinner.Tests name for the test project we create, and use the Visual Studio Unit Test Framework option. When we click the OK button, Visual Studio will create a solution for us with two projects in it — one for our web application and one for our unit tests (Figure 1-10):

Image from book
Figure 1-10

Examining the NerdDinner Directory Structure

When you create a new ASP.NET MVC application with Visual Studio, it automatically adds a number of files and directories to the project, as shown in Figure 1-11.

Image from book
Figure 1-11

ASP.NET MVC projects by default have six top-level directories, shown in the following table:

Open table as spreadsheet

Directory

Purpose

/Controllers

Where you put Controller classes that handle URL requests

/Models

Where you put classes that represent and manipulate data

/Views

Where you put UI template files that are responsible for rendering output

/Scripts

Where you put JavaScript library files and scripts (.js)

/Content

Where you put CSS and image files, and other non-dynamic/non-JavaScript content

/App_Data

Where you store data files you want to read/write.

ASP.NET MVC does not require this structure. In fact, developers working on large applications will typically partition the application up across multiple projects to make it more manageable (for example: data model classes often go in a separate class library project from the web application). The default project structure, however, does provide a nice default directory convention that we can use to keep our application concerns clean.

When we expand the /Controllers directory, we'll find that Visual Studio added two controller classes (Figure 1-12) — HomeController and AccountController — by default to the project:

Image from book
Figure 1-12

When we expand the /Views directory, we'll find three subdirectories — /Home, /Account and /Shared — as well as several template files within them, were also added to the project by default (Figure 1-13):

Image from book
Figure 1-13

When we expand the /Content and /Scripts directories, we'll find a Site.css file that is used to style all HTML on the site, as well as JavaScript libraries that can enable ASP.NET AJAX and jQuery support within the application (Figure 1-14):

Image from book
Figure 1-14

When we expand the NerdDinner.Tests project we'll find two classes that contain unit tests for our controller classes (Figure 1-15):

Image from book
Figure 1-15

These default files, added by Visual Studio, provide us with a basic structure for a working application — complete with home page, about page, account login/logout/registration pages, and an unhandled error page (all wired-up and working out of the box).

Running the NerdDinner Application

We can run the project by choosing either the Debug ð Start Debugging or Debug ð Start Without Debugging menu items (Figure 1-16):

Image from book
Figure 1-16

This will launch the built-in ASP.NET web server that comes with Visual Studio, and run our application (Figure 1-17):

Image from book
Figure 1-17

FIgure 1-18 is the home page for our new project (URL: /) when it runs:

Image from book
Figure 1-18

Clicking the About tab displays an About page (URL: /Home/About, shown in Figure 1-19):

Image from book
Figure 1-19

Clicking the Log On link on the top right takes us to a Login page shown in Figure 1-20 (URL: /Account/LogOn)

Image from book
Figure 1-20

If we don't have a login account, we can click the Register link (URL: /Account/Register) to create one (Figure 1-21):

Image from book
Figure 1-21

The code to implement the above home, about, and login/register functionality was added by default when we created our new project. We'll use it as the starting point of our application.

Testing the NerdDinner Application

If we are using the Professional Edition or higher version of Visual Studio 2008, we can use the built-in unit-testing IDE support within Visual Studio to test the project.

Choosing one of the above options in Figure 1-22 will open the Test Results pane within the IDE (Figure 1-23) and provide us with pass/fail status on the 27 unit tests included in our new project that cover the built-in functionality.

Image from book
Figure 1-22
Image from book
Figure 1-23
 
 

Creating the Database

We'll be using a database to store all of the Dinner and RSVP data for our NerdDinner application.

The steps below show creating the database using the free SQL Server Express edition. All of the code we'll write works with both SQL Server Express and the full SQL Server.

Creating a New SQL Server Express Database

We'll begin by right-clicking on our web project, and then selecting the Add ð New Item menu command (Figure 1-24).

Image from book
Figure 1-24

This will bring up the Add New Item dialog (Figure 1-25). We'll filter by the Data category and select the SQL Server Database item template.

Image from book
Figure 1-25

We'll name the SQL Server Express database we want to create NerdDinner.mdf and hit OK. Visual Studio will then ask us if we want to add this file to our \App_Data directory (Figure 1-26), which is a directory already set up with both read and write security ACLs.

Image from book
Figure 1-26

We'll click Yes and our new database will be created and added to our Solution Explorer (Figure 1-27).

Image from book
Figure 1-27

Creating Tables within Our Database

We now have a new empty database. Let's add some tables to it.

To do this we'll navigate to the Server Explorer tab window within Visual Studio, which enables us to manage databases and servers. SQL Server Express databases stored in the \App_Data folder of our application will automatically show up within the Server Explorer. We can optionally use the Connect to Database icon on the top of the Server Explorer window to add additional SQL Server databases (both local and remote) to the list as well (Figure 1-28).

Image from book
Figure 1-28

We will add two tables to our NerdDinner database — one to store our Dinners, and the other to track RSVP acceptances to them. We can create new tables by right-clicking on the Tables folder within our database and choosing the Add New Table menu command (Figure 1-29).

Image from book
Figure 1-29

This will open up a table designer that allows us to configure the schema of our table. For our Dinners table, we will add 10 columns of data (Figure 1-30).

Image from book
Figure 1-30

We want the DinnerID column to be a unique primary key for the table. We can configure this by right-clicking on the DinnerID column and choosing the Set Primary Key menu item (Figure 1-31).

Image from book
Figure 1-31

In addition to making DinnerID a primary key, we also want configure it as an identity column whose value is automatically incremented as new rows of data are added to the table (meaning the first inserted Dinner row will have a DinnerID of 1, the second inserted row will have a DinnerID of 2, etc.).

We can do this by selecting the DinnerID column and then using the Column Properties editor to set the "(Is Identity)" property on the column to Yes (Figure 1-32). We will use the standard identity defaults (start at 1 and increment 1 on each new Dinner row).

Image from book
Figure 1-32

We'll then save our table by pressing Ctrl-S or by clicking the File ð Save menu command. This will prompt us to name the table. We'll name it Dinners (Figure 1-33).

Image from book
Figure 1-33

Our new Dinners table will then show up in our database in the Server Explorer.

We'll then repeat the above steps and create a RSVP table. This table will have three columns. We will set up the RsvpID column as the primary key, and also make it an identity column (Figure 1-34).

Image from book
Figure 1-34

We'll save it and give it the name RSVP.

Setting Up a Foreign Key Relationship Between Tables

We now have two tables within our database. Our last schema design step will be to set up a "one-to-many" relationship between these two tables — so that we can associate each Dinner row with zero or more RSVP rows that apply to it. We will do this by configuring the RSVP table's DinnerID column to have a foreign-key relationship to the DinnerID column in the Dinners table.

To do this we'll open up the RSVP table within the table designer by double-clicking it in the Server Explorer. We'll then select the DinnerID column within it, right-click, and choose the Relationships context menu command (Figure 1-35):

Image from book
Figure 1-35

This will bring up a dialog that we can use to set up relationships between tables (Figure 1-36)

Image from book
Figure 1-36

We'll click the Add button to add a new relationship to the dialog. Once a relationship has been added, we'll expand the Tables and Column Specification tree-view node within the property grid to the right of the dialog, and then click the "" button to the right of it (Figure 1-37).

Image from book
Figure 1-37

Clicking the "" button will bring up another dialog that allows us to specify which tables and columns are involved in the relationship, as well as allow us to name the relationship.

We will change the Primary Key Table to be Dinners, and select the DinnerID column within the Dinners table as the primary key. Our RSVP table will be the foreign-key table, and the RSVP.DinnerID column will be associated as the foreign-key (Figure 1-38).

Image from book
Figure 1-38

Now each row in the RSVP table will be associated with a row in the Dinner table. SQL Server will maintain referential integrity for us — and prevent us from adding a new RSVP row if it does not point to a valid Dinner row. It will also prevent us from deleting a Dinner row if there are still RSVP rows referring to it.

Adding Data to Our Tables

Let's finish by adding some sample data to our Dinners table. We can add data to a table by right-clicking on it in the Server Explorer and choosing the Show Table Data command (Figure 1-39):

Image from book
Figure 1-39

Let's add a few rows of Dinner data that we can use later as we start implementing the application (Figure 1-40).

Image from book
Figure 1-40
 

Partials and Master Pages

One of the design philosophies ASP.NET MVC embraces is the Do Not Repeat Yourself principle (commonly referred to as DRY). A DRY design helps eliminate the duplication of code and logic, which ultimately makes applications faster to build and easier to maintain.

We've already seen the DRY principle applied in several of our NerdDinner scenarios. A few examples: our validation logic is implemented within our model layer, which enables it to be enforced across both edit and create scenarios in our controller; we are reusing the "NotFound" view template across the Edit, Details and Delete action methods; we are using a convention-naming pattern with our view templates, which eliminates the need to explicitly specify the name when we call the View helper method; and we are reusing the DinnerFormViewModel class for both Edit and Create action scenarios.

Let's now look at ways we can apply the DRY Principle within our view templates to eliminate code duplication there as well.

Revisiting Our Edit and Create View Templates

Currently we are using two different view templates — Edit.aspx and Create.aspx — to display our Dinner form UI. A quick visual comparison of them highlights how similar they are. Figure 1-101 shows what the create form looks like:

Image from book
Figure 1-101

And Figure 1-102 is what our "Edit" form looks like.

Image from book
Figure 1-102

Not much of a difference is there? Other than the title and header text, the form layout and input controls are identical.

If we open up the Edit.aspx and Create.aspx view templates, we'll find that they contain identical form layout and input control code. This duplication means we end up having to make changes twice anytime we introduce or change a new Dinner property — which is not good.

Using Partial View Templates

ASP.NET MVC supports the ability to define partial view templates that can be used to encapsulate view rendering logic for a sub-portion of a page. Partials provide a useful way to define view rendering logic once, and then reuse it in multiple places across an application.

To help "DRY-up" our Edit.aspx and Create.aspx View template duplication, we can create a partial View template named DinnerForm.ascx that encapsulates the form layout and input elements common to both. We'll do this by right-clicking on our \Views\Dinners directory and choosing the Add ð View menu command shown in Figure 1-103:

Image from book
Figure 1-103

This will display the Add View dialog. We'll name the new view we want to create DinnerForm, select the "Create a partial view" checkbox on the dialog, and indicate that we will pass it a DinnerFormViewModel class (see Figure 1-104).

Image from book
Figure 1-104

When we click the Add button, Visual Studio will create a new DinnerForm.ascx view template for us within the \Views\Dinners directory.

We can then copy/paste the duplicate form layout/input control code from our Edit.aspx/ Create.aspx view templates into our new DinnerForm.ascx partial view template:

<%= Html.ValidationSummary("Please correct the errors and try again.") %><% using (Html.BeginForm()) { %>    <fieldset>        <p>            <label for=" Title">Dinner Title:</label>            <%= Html.TextBox("Title", Model.Dinner.Title) %>            <%= Html.ValidationMessage("Title", "*") %>        </p>        <p>            <label for=" EventDate">Event Date:</label>            <%= Html.TextBox("EventDate", Model.Dinner.EventDate) %>            <%= Html.ValidationMessage("EventDate", "*") %>        </p>        <p>            <label for=" Description">Description:</label>            <%= Html.TextArea("Description", Model.Dinner.Description) %>            <%= Html.ValidationMessage("Description", "*")%>        </p>        <p>            <label for=" Address">Address:</label>            <%= Html.TextBox("Address", Model.Dinner.Address) %>            <%= Html.ValidationMessage("Address", "*") %>        </p>        <p>            <label for=" Country">Country:</label>            <%= Html.DropDownList("Country", Model.Countries) %>            <%= Html.ValidationMessage("Country", "*") %>        </p>        <p>            <label for=" ContactPhone">Contact Phone #:</label>            <%= Html.TextBox("ContactPhone", Model.Dinner.ContactPhone) %>            <%= Html.ValidationMessage("ContactPhone", "*") %>        </p>        <p>            <input type=" submit" value=" Save" />        </p>    </fieldset><% } %>

We can then update our Edit and Create view templates to call the DinnerForm partial template and eliminate the form duplication. We can do this by calling Html.RenderPartial("DinnerForm") within our view templates:

Create.aspx

<asp:Content ID=" Title" ContentPlaceHolderID=" TitleContent" runat=" server">    Host a Dinner</asp:Content><asp:Content ID=" Create" ContentPlaceHolderID=" MainContent" runat=" server">    <h2>Host a Dinner</h2>    <% Html.RenderPartial("DinnerForm"); %></asp:Content>

Edit.aspx

<asp:Content ID=" Title" ContentPlaceHolderID=" TitleContent" runat=" server">    Edit: <%=Html.Encode(Model.Dinner.Title) %></asp:Content><asp:Content ID=" Edit" ContentPlaceHolderID=" MainContent" runat=" server">    <h2>Edit Dinner</h2>    <% Html.RenderPartial("DinnerForm"); %></asp:Content>

You can explicitly qualify the path of the partial template you want when calling Html.RenderPartial (for example: ~/Views/Dinners/DinnerForm.ascx). In our previous code, though, we are taking advantage of the convention-based naming pattern within ASP.NET MVC, and just specifying DinnerForm as the name of the partial to render. When we do this, ASP.NET MVC will look first in the convention-based views directory (for DinnersController this would be /Views/Dinners). If it doesn't find the partial template there, it will then look for it in the /Views/Shared directory.

When Html.RenderPartial is called with just the name of the partial view, ASP.NET MVC will pass to the partial view the same Model and ViewData dictionary objects used by the calling view template. Alternatively, there are overloaded versions of Html.RenderPartial that enable you to pass an alternate Model object and/or ViewData dictionary for the partial view to use. This is useful for scenarios where you only want to pass a subset of the full Model/ViewModel.

Using Partial View Templates to Clarify Code

We created the DinnerForm partial view template to avoid duplicating view rendering logic in multiple places. This is the most common reason to create partial view templates.

Sometimes it still makes sense to create partial views even when they are only being called in a single place. Very complicated view templates can often become much easier to read when their view rendering logic is extracted and partitioned into one or more well-named partial templates.

For example, consider the below code-snippet from the e file in our project (which we will be looking at shortly). The code is relatively straightforward to read — partly because the logic to display a login/logout link at the top right of the screen is encapsulated within the LogOnUserControl partial:

<div id=" header">    <div id=" title">        <h1>My MVC Application</h1>    </div>    <div id=" logindisplay">        <% Html.RenderPartial("LogOnUserControl"); %>    </div>    <div id=" menucontainer">        <ul id=" menu">            <li><%= Html.ActionLink("Home", "Index", "Home")%></li>            <li><%= Html.ActionLink("About", "About", "Home")%></li>        </ul>    </div></div>

Whenever you find yourself getting confused trying to understand the HTML/code markup within a view template, consider whether it wouldn't be clearer if some of it was extracted and refactored into well-named partial views.

Master Pages

In addition to supporting partial views, ASP.NET MVC also supports the ability to create master page templates that can be used to define the common layout and top-level HTML of a site. Content placeholder controls can then be added to the master page to identify replaceable regions that can be overridden or filled in by views. This provides a very effective (and DRY) way to apply a common layout across an application.

By default, new ASP.NET MVC projects have a master page template automatically added to them. This master page is named Site.master and lives within the \Views\Shared\ folder as shown in Figure 1-105.

Image from book
Figure 1-105

The default Site.master file looks like the following code. It defines the outer HTML of the site, along with a menu for navigation at the top. It contains two replaceable content placeholder controls — one for the title, and the other for where the primary content of a page should be replaced:

<%@ Master Language=" C#" Inherits=" System.Web.Mvc.ViewMasterPage" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns=" http://www.w3.org/1999/xhtml"><head runat=" server">   <title><asp:ContentPlaceHolder ID=" TitleContent" runat=" server" /></title>   <link href="http://www.cnblogs.com/Content/Site.css" rel=" stylesheet" type=" text/css" /></head><body>    <div class="page ">        <div id=" header">            <div id=" title">                <h1>My MVC Application</h1>            </div>            <div id=" logindisplay">                <% Html.RenderPartial("LogOnUserControl"); %>            </div>            <div id=" menucontainer">                <ul id=" menu">                    <li><%= Html.ActionLink("Home", "Index", "Home")%></li>                    <li><%= Html.ActionLink("About", "About", "Home")%></li>                </ul>            </div>        </div>        <div id=" main">            <asp:ContentPlaceHolder ID=" MainContent" runat=" server" />        </div>    </div></body></html>

All of the view templates we've created for our NerdDinner application ("List", "Details", "Edit", "Create", "NotFound", etc.) have been based on this Site.master template. This is indicated via the MasterPageFile attribute that was added by default to the top <% @ Page %> directive when we created our views using the Add View dialog:

<%@ Page Language=" C#" Inherits=" System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerViewModel>" MasterPageFile="~/Views/Shared/Site.Master" %>

What this means is that we can change the Site.master content, and have the changes automatically be applied and used when we render any of our view templates.

Let's update our Site.master's header section so that the header of our application is "NerdDinner" instead of "My MVC Application." Let's also update our navigation menu so that the first tab is "Find a Dinner" (handled by the HomeController's Index action method), and let's add a new tab called "Host a Dinner" (handled by the DinnersController's Create action method):

<div id=" header">    <div id=" title">        <h1>NerdDinner</h1>    </div>    <div id=" logindisplay">        <% Html.RenderPartial("LoginStatus"); %>    </div>    <div id=" menucontainer">        <ul id=" menu">           <li><%= Html.ActionLink("Find Dinner", "Index", "Home")%></li>           <li><%= Html.ActionLink("Host Dinner", "Create", "Dinners")%></li>           <li><%= Html.ActionLink("About", "About", "Home")%></li>        </ul>    </div></div>

When we save the Site.master file and refresh our browser, we'll see our header changes show up across all views within our application. For example, see Figure 1-106.

Image from book
Figure 1-106

And with the /Dinners/Edit/[id] URL (Figure 1-107):

Image from book
Figure 1-107

Partials and master pages provide very flexible options that enable you to cleanly organize views. You'll find that they help you avoid duplicating view content/code, and make your view templates easier to read and maintain.

Paging Support

If our site is successful, it will have thousands of upcoming dinners. We need to make sure that our UI scales to handle all of these dinners and allows users to browse them. To enable this, we'll add paging support to our /Dinners URL so that instead of displaying thousands of dinners at once, we'll only display 10 upcoming dinners at a time — and allow end users to page back and forward through the entire list in an SEO friendly way.

Index() Action Method Recap

The Index action method within our DinnersController class currently looks like the following code:

//// GET: /Dinners/public ActionResult Index() {    var dinners = dinnerRepository.FindUpcomingDinners().ToList();    return View(dinners);}

When a request is made to the /Dinners URL, it retrieves a list of all upcoming dinners and then renders a listing of all of them (Figure 1-108):

Image from book
Figure 1-108

Understanding IQueryable<T>

IQueryable<T> is an interface that was introduced with LINQ in .NET 3.5. It enables powerful deferred execution scenarios that we can take advantage of to implement paging support.

In our DinnerRepository in the following code we are returning an IQueryable<Dinner> sequence from our FindUpcomingDinners method:

public class DinnerRepository {    private NerdDinnerDataContext db = new NerdDinnerDataContext();    //    // Query Methods    public IQueryable<Dinner> FindUpcomingDinners() {        return from dinner in db.Dinners               where dinner.EventDate > DateTime.Now               orderby dinner.EventDate               select dinner;    }

The IQueryable<Dinner> object returned by our FindUpcomingDinners method encapsulates a query to retrieve Dinner objects from our database using LINQ to SQL. Importantly, it won't execute the query against the database until we attempt to access/iterate over the data in the query, or until we call the ToListmethod on it. The code calling our FindUpcomingDinners method can optionally choose to add additional "chained" operations/filters to the IQueryable<Dinner> object before executing the query. LINQ to SQL is then smart enough to execute the combined query against the database when the data is requested.

To implement paging logic, we can update our Index action method so that it applies additional Skip and Take operators to the returned IQueryable<Dinner> sequence before calling ToList on it:

//// GET: /Dinners/public ActionResult Index() {    var upcomingDinners = dinnerRepository.FindUpcomingDinners();    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();    return View(paginatedDinners);}

The above code skips over the first 10 upcoming dinners in the database, and then returns 20 dinners. LINQ to SQL is smart enough to construct an optimized SQL query that performs this skipping logic in the SQL database — and not in the web server. This means that even if we have millions of upcoming dinners in the database, only the 10 we want will be retrieved as part of this request (making it efficient and scalable).

Adding a "page" Value to the URL

Instead of hard-coding a specific page range, we'll want our URLs to include a page parameter that indicates which Dinner range a user is requesting.

Using a Querystring Value

The code that follows demonstrates how we can update our Index action method to support a querystring parameter and enable URLs like /Dinners?page=2:

//// GET: /Dinners///      /Dinners?page=2public ActionResult Index(int? page) {    const int pageSize = 10;    var upcomingDinners = dinnerRepository.FindUpcomingDinners();    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)                                          .Take(pageSize)                                          .ToList();    return View(paginatedDinners);}

The Index action method in the previous code has a parameter named page. The parameter is declared as a nullable integer. This means that the /Dinners?page=2 URL will cause a value of "2" to be passed as the parameter value. The /Dinners URL (without a querystring value) will cause a null value to be passed.

We are multiplying the page value by the page size (in this case 10 rows) to determine how many dinners to skip over. We are using the C# "coalescing" operator (??) which is useful when dealing with nullable types. The previous code assigns page the value of 0 if the page parameter is null.

Using Embedded URL Values

An alternative to using a querystring value would be to embed the page parameter within the actual URL itself. For example: /Dinners/Page/2 or /Dinners/2. ASP.NET MVC includes a powerful URL routing engine that makes it easy to support scenarios like this.

We can register custom routing rules that map any incoming URL or URL format to any controller class or action method we want. All we need to do is to open the Global.asax file within our project (Figure 1-109).

Image from book
Figure 1-109

And then register a new mapping rule using the MapRoute helper method as in the first call to routes.MapRoute that follows:

public void RegisterRoutes(RouteCollection routes) {    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");    routes.MapRoute(        "UpcomingDinners",        "Dinners/Page/{page}",        new { controller = "Dinners", action = "Index" }    );    routes.MapRoute(        "Default",                                          // Route name        "{controller}/{action}/{id}",                       // URL with params        new { controller=" Home", action=" Index", id=" "}  // Param defaults    );}void Application_Start() {    RegisterRoutes(RouteTable.Routes);}

In the previous code, we are registering a new routing rule named "UpcomingDinners". We are indicating it has the URL format "Dinners/Page/{page}" — where {page} is a parameter value embedded within the URL. The third parameter to the MapRoute method indicates that we should map URLs that match this format to the Index action method on the DinnersController class.

We can use the exact same Index code we had before with our Querystring scenario — except now our page parameter will come from the URL and not the querystring:

//// GET: /Dinners///      /Dinners/Page/2public ActionResult Index(int? page) {    const int pageSize = 10;    var upcomingDinners = dinnerRepository.FindUpcomingDinners();    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)                                          .Take(pageSize)                                          .ToList();    return View(paginatedDinners);}

And now when we run the application and type in /Dinners, we'll see the first 10 upcoming dinners, as shown in Figure 1-110.

Image from book
Figure 1-110

And when we type in /Dinners/Page/1, we'll see the next page of dinners (Figure 1-111):

Image from book
Figure 1-111

Adding Page Navigation UI

The last step to complete our paging scenario will be to implement "next" and "previous" navigation UI within our view template to enable users to easily skip over the Dinner data.

To implement this correctly, we'll need to know the total number of Dinners in the database, as well as how many pages of data this translates to. We'll then need to calculate whether the currently requested "page" value is at the beginning or end of the data, and show or hide the "previous" and "next" UI accordingly. We could implement this logic within our Index action method. Alternatively, we can add a helper class to our project that encapsulates this logic in a more reusable way.

The following code is a simple PaginatedList helper class that derives from the List<T> collection class built into the .NET Framework. It implements a reusable collection class that can be used to paginate any sequence of IQueryable data. In our NerdDinner application we'll have it work over IQueryable<Dinner> results, but it could just as easily be used against IQueryable<Product> or IQueryable<Customer> results in other application scenarios:

public class PaginatedList<T> : List<T> {    public int PageIndex  { get; private set; }    public int PageSize   { get; private set; }    public int TotalCount { get; private set; }    public int TotalPages { get; private set; }    public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {        PageIndex = pageIndex;        PageSize = pageSize;        TotalCount = source.Count();        TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);        this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));    }    public bool HasPreviousPage {        get {            return (PageIndex > 0);        }    }    public bool HasNextPage {        get {            return (PageIndex+1 < TotalPages);        }    }}

Notice in the previous code how it calculates and then exposes properties like PageIndex, PaegeSize, TotalCount, and TotalPages. It also then exposes two helper properties HasPreviousPage and HasNextPage that indicate whether the page of data in the collection is at the beginning or end of the original sequence. The above code will cause two SQL queries to be run — the first to retrieve the count of the total number of Dinner objects (this doesn't return the objects — rather it performs a SELECT COUNT statement that returns an integer), and the second to retrieve just the rows of data we need from our database for the current page of data.

We can then update our DinnersController.Index helper method to create a PaginatedList<Dinner> from our DinnerRepository.FindUpcomingDinners result, and pass it to our view template:

//// GET: /Dinners///      /Dinners/Page/2public ActionResult Index(int? page) {    const int pageSize = 10;    var upcomingDinners = dinnerRepository.FindUpcomingDinners();    var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners,                                                     page ?? 0,                                                     pageSize);    return View(paginatedDinners);}

We can then update the \Views\Dinners\Index.aspx view template to inherit from ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> instead of ViewPage<IEnumerable<Dinner>>, and then add the following code to the bottom of our view template to show or hide next and previous navigation UI:

<% if (Model.HasPreviousPage) { %>    <%= Html.RouteLink("<<<",                       "UpcomingDinners",                       new { page=(Model.PageIndex-1) }) %><% } %><% if (Model.HasNextPage) { %>    <%= Html.RouteLink(">>>",                       "UpcomingDinners",                       new { page = (Model.PageIndex + 1) })%><% } %>

Notice, in the previous code, how we are using the Html.RouteLink helper method to generate our hyperlinks. This method is similar to the Html.ActionLink helper method we've used previously. The difference is that we are generating the URL using the "UpcomingDinners" routing rule we set up within our Global.asax file. This ensures that we'll generate URLs to our Index action method that have the format: /Dinners/Page/{page} — where the {page} value is a variable we are providing above based on the current PageIndex.

And now when we run our application again, we'll see 10 dinners at a time in our browser, as shown in Figure 1-112.

Image from book
Figure 1-112

We also have <<< and >>> navigation UI at the bottom of the page that allows us to skip forwards and backwards over our data using search-engine-accessible URLs (Figure 1-113).

Image from book
Figure 1-113
 

Authentication and Authorization

Right now our NerdDinner application grants anyone visiting the site the ability to create and edit the details of any dinner. Let's change this so that users need to register and log in to the site to create new dinners, and add a restriction so that only the user who is hosting a dinner can edit it later.

To enable this we'll use authentication and authorization to secure our application.

Understanding Authentication and Authorization

Authentication is the process of identifying and validating the identity of a client accessing an application. Put more simply, it is about identifying who the end user is when they visit a website.

ASP.NET supports multiple ways to authenticate browser users. For Internet web applications, the most common authentication approach used is called Forms Authentication. Forms Authentication enables a developer to author an HTML login form within their application and then validate the username/password an end user submits against a database or other password credential store. If the username/password combination is correct, the developer can then ask ASP.NET to issue an encrypted HTTP cookie to identify the user across future requests. We'll be using forms authentication with our NerdDinner application.

Authorization is the process of determining whether an authenticated user has permission to access a particular URL/resource or to perform some action. For example, within our NerdDinner application we'll want to authorize only users who are logged in to access the /Dinners/Create URL and create new dinners. We'll also want to add authorization logic so that only the user who is hosting a dinner can edit it — and deny edit access to all other users.

Forms Authentication and the AccountController

The default Visual Studio project template for ASP.NET MVC automatically enables forms authentication when new ASP.NET MVC applications are created. It also automatically adds a pre-built account login implementation to the project — which makes it really easy to integrate security within a site.

The default Site.master master page displays a [Log On] link (shown in Figure 1-114) at the top right of the site when the user accessing it is not authenticated:

Image from book
Figure 1-114

Clicking the [Log On] link takes a user to the /Account/LogOn URL (Figure 1-115)

Image from book
Figure 1-115

Visitors who haven't registered can do so by clicking the Register link — which will take them to the /Account/Register URL and allow them to enter account details (Figure 1-116).

Image from book
Figure 1-116

Clicking the Register button will create a new user within the ASP.NET Membership system, and authenticate the user onto the site using forms authentication.

When a user is logged in, the Site.master changes the top right of the page to output a "Welcome [username]!" message and renders a [Log Off] link instead of a [Log On] one. Clicking the [Log Off] link logs out the user (Figure 1-117).

Image from book
Figure 1-117

The above login, logout, and registration functionality is implemented within the AccountController class that was added to our project by VS when it created it. The UI for the AccountController is implemented using view templates within the \Views\Account directory (Figure 1-118).

Image from book
Figure 1-118

The AccountController class uses the ASP.NET Forms Authentication system to issue encrypted authentication cookies, and the ASP.NET Membership API to store and validate usernames/passwords. The ASP. NET Membership API is extensible and enables any password credential store to be used. ASP.NET ships with built-in membership provider implementations that store username/passwords within a SQL database, or within Active Directory.

We can configure which membership provider our NerdDinner application should use by opening the web.config file at the root of the project and looking for the <membership> section within it. The default web.config, added when the project was created, registers the SQL membership provider, and configures it to use a connection-string named ApplicationServices to specify the database location.

The default ApplicationServices connection string (which is specified within the <connectionStrings> section of the web.config file) is configured to use SQL Express. It points to a SQL Express database named ASPNETDB.MDF under the application's App_Data directory. If this database doesn't exist the first time the Membership API is used within the application, ASP.NET will automatically create the database and provision the appropriate membership database schema within it (Figure 1-119).

Image from book
Figure 1-119

If instead of using SQL Express we wanted to use a full SQL Server instance (or connect to a remote database), all we'd need to do is to update the ApplicationServices connection string within the web.config file and make sure that the appropriate membership schema has been added to the database it points at. You can run the aspnet_regsql.exe utility within the \Windows\Microsoft.NET\Framework\v2.0.50727\ directory to add the appropriate schema for membership and the other ASP.NET application services to a database.

Authorizing the /Dinners/Create URL Using the [Authorize] Filter

We didn't have to write any code to enable a secure authentication and account management implementation for the NerdDinner application. Users can register new accounts with our application, and log in/log out of the site. And now we can add authorization logic to the application, and use the authentication status and username of visitors to control what they can and can't do within the site.

Let's begin by adding authorization logic to the Create action methods of our DinnersController class. Specifically, we will require that users accessing the /Dinners/Create URL must be logged in. If they aren't logged in, we'll redirect them to the login page so that they can sign in.

Implementing this logic is pretty easy. All we need to do is to add an [Authorize] filter attribute to our Create action methods like so:

//// GET: /Dinners/Create[Authorize]public ActionResult Create() {   ...}//// POST: /Dinners/Create[AcceptVerbs(HttpVerbs.Post), Authorize]public ActionResult Create(Dinner dinnerToCreate) {   ...}

ASP.NET MVC supports the ability to create action filters that can be used to implement reusable logic that can be declaratively applied to action methods. The [Authorize] filter is one of the built-in action filters provided by ASP.NET MVC, and it enables a developer to declaratively apply authorization rules to action methods and controller classes.

When applied without any parameters (as in the previous code), the [Authorize] filter enforces that the user making the action method request must be logged in — and it will automatically redirect the browser to the login URL if they aren't. When doing this redirect, the originally requested URL is passed as a querystring argument (for example: /Account/LogOn?ReturnUrl=%2fDinners%2fCreate). The AccountController will then redirect the user back to the originally requested URL once they log in.

The [Authorize] filter optionally supports the ability to specify a Users or Roles property that can be used to require that the user is both logged in and within a list of allowed users or a member of an allowed security role. For example, the code below only allows two specific users, scottgu and billg, to access the /Dinners/Create URL:

[Authorize(Users=" scottgu,billg")]public ActionResult Create() {    ...}

Embedding specific user names within code tends to be pretty unmaintainable though. A better approach is to define higher-level roles that the code checks against, and then to map users into the role using either a database or active directory system (enabling the actual user mapping list to be stored externally from the code). ASP.NET includes a built-in role management API as well as a built-in set of role providers (including ones for SQL and Active Directory) that can help perform this user/role mapping. We could then update the code to only allow users within a specific "admin" role to access the /Dinners/Create URL:

[Authorize(Roles=" admin")]public ActionResult Create() {   ...}

Using the User.Identity.Name Property When Creating Dinners

We can retrieve the username of the currently logged-in user of a request using the User.Identity.Name property exposed on the Controller base class.

Earlier, when we implemented the HTTP-POST version of our Create action method, we had hard-coded the HostedBy property of the dinner to a static string. We can now update this code to instead use the User.Identity.Name property, as well as automatically add an RSVP for the host creating the dinner:

//// POST: /Dinners/Create[AcceptVerbs(HttpVerbs.Post), Authorize]public ActionResult Create(Dinner dinner) {    if (ModelState.IsValid) {        try {            dinner.HostedBy = User.Identity.Name;            RSVP rsvp = new RSVP();            rsvp.AttendeeName = User.Identity.Name;            dinner.RSVPs.Add(rsvp);            dinnerRepository.Add(dinner);            dinnerRepository.Save();            return RedirectToAction("Details", new { id=dinner.DinnerID });        }        catch {            ModelState.AddModelErrors(dinner.GetRuleViolations());        }    }    return View(new DinnerFormViewModel(dinner));}

Because we have added an [Authorize] attribute to the Create method, ASP.NET MVC ensures that the action method only executes if the user visiting the /Dinners/Create URL is logged in on the site. As such, the User.Identity.Name property value will always contain a valid username.

Using the User.Identity.Name Property When Editing Dinners

Let's now add some authorization logic that restricts users so that they can only edit the properties of dinners they themselves are hosting.

To help with this, we'll first add an IsHostedBy(username) helper method to our Dinner object (within the Dinner.cs partial class we built earlier). This helper method returns true or false, depending on whether a supplied username matches the Dinner HostedBy property, and encapsulates the logic necessary to perform a case-insensitive string comparison of them:

public partial class Dinner {    public bool IsHostedBy(string userName) {    return HostedBy.Equals(userName,                           StringComparison.InvariantCultureIgnoreCase);    }}

We'll then add an [Authorize] attribute to the Edit action methods within our DinnersController class. This will ensure that users must be logged in to request a /Dinners/Edit/[id] URL.

We can then add code to our Edit methods that uses the Dinner.IsHostedBy(username) helper method to verify that the logged-in user matches the dinner host. If the user is not the host, we'll display an "InvalidOwner" view and terminate the request. The code to do this looks like the following:

//// GET: /Dinners/Edit/5[Authorize]public ActionResult Edit(int id) {    Dinner dinner = dinnerRepository.GetDinner(id);    if (!dinner.IsHostedBy(User.Identity.Name))        return View("InvalidOwner");    return View(new DinnerFormViewModel(dinner));}//// POST: /Dinners/Edit/5[AcceptVerbs(HttpVerbs.Post), Authorize]public ActionResult Edit(int id, FormCollection collection) {    Dinner dinner = dinnerRepository.GetDinner(id);    if (!dinner.IsHostedBy(User.Identity.Name))        return View("InvalidOwner");    try {        UpdateModel(dinner);        dinnerRepository.Save();        return RedirectToAction("Details", new {id = dinner.DinnerID});    }    catch {        ModelState.AddModelErrors(dinnerToEdit.GetRuleViolations());        return View(new DinnerFormViewModel(dinner));    }}

We can then right-click on the \Views\Dinners directory and choose the Add ð View menu command to create a new "InvalidOwner" view. We'll populate it with the following error message:

<asp:Content ID=" Title" ContentPlaceHolderID=" TitleContent" runat=" server">      You Don't Own This Dinner</asp:Content><asp:Content ID=" Main" ContentPlaceHolderID=" MainContent" runat=" server">    <h2>Error Accessing Dinner</h2>    <p>Sorry - but only the host of a Dinner can edit or delete it.</p></asp:Content>

And now when a user attempts to edit a dinner they don't own, they'll get the error message shown in Figure 1-120.

Image from book
Figure 1-120

We can repeat the same steps for the Delete action methods within our controller to lock down permission to delete dinners as well, and ensure that only the host of a dinner can delete it.

Showing/Hiding Edit and Delete Links

We are linking to the Edit and Delete action method of our DinnersController class from our /Details URL (Figure 1-121).

Image from book
Figure 1-121

Currently we are showing the Edit and Delete action links regardless of whether the visitor to the details URL is the host of the dinner. Let's change this so that the links are only displayed if the visiting user is the owner of the dinner.

The Details action method within our DinnersController retrieves a Dinner object and then passes it as the model object to our view template:

//// GET: /Dinners/Details/5public ActionResult Details(int id) {    Dinner dinner = dinnerRepository.GetDinner(id);    if (dinner == null)        return View("NotFound");    return View(dinner);}

We can update our view template to conditionally show/hide the Edit and Delete links by using the Dinner.IsHostedBy helper method as in the code that follows:

<% if (Model.IsHostedBy(Context.User.Identity.Name)) { %>   <%= Html.ActionLink("Edit Dinner", "Edit", new { id=Model.DinnerID })%> |   <%= Html.ActionLink("Delete Dinner", "Delete", new {id=Model.DinnerID})%><% } %>
 

AJAX Enabling RSVPs Accepts

Let's now add support for logged-in users to RSVP their interest in attending a dinner. We'll implement this using an AJAX-based approach integrated within the dinner details page.

Indicating Whether the User Is RSVP'ed

Users can visit the /Dinners/Details/[id] URL to see details about a particular dinner (Figure 1-122).

Image from book
Figure 1-122

The Details action method is implemented like so:

//// GET: /Dinners/Details/2public ActionResult Details(int id) {    Dinner dinner = dinnerRepository.GetDinner(id);    if (dinner == null)        return View("NotFound");    else        return View(dinner);}

Our first step to implement RSVP support will be to add an IsUserRegistered(username) helper method to our Dinner object (within the Dinner.cs partial class we built earlier). This helper method returns true or false, depending on whether the user is currently RSVP'd for the dinner:

public partial class Dinner {    public bool IsUserRegistered(string userName) {        return RSVPs.Any(r => r.AttendeeName.Equals(userName,                                StringComparison.InvariantCultureIgnoreCase));    }}

We can then add the following code to our Details.aspx view template to display an appropriate message indicating whether the user is registered or not for the event:

<% if (Request.IsAuthenticated) { %>    <% if (Model.IsUserRegistered(Context.User.Identity.Name)) { %>        <p>You are registered for this event!</p>    <% } else { %>        <p>You are not registered for this event</p>    <% } %><% } else { %>    <a href="/Account/Logon">Logon</a> to RSVP for this event.<% } %>

And now when a user visits a dinner they are registered for they'll see the message in Figure 1-123.

Image from book
Figure 1-123

And when they visit a dinner they are not registered for, they'll see the message in Figure 1-124.

Image from book
Figure 1-124

Implementing the Register Action Method

Let's now add the functionality necessary to enable users to RSVP for a dinner from the details page.

To implement this, we'll create a new RSVPController class by right-clicking on the \Controllers directory and choosing the Add ð Controller menu command.

We'll implement a Register action method within the new RSVPController class that takes an ID for a dinner as an argument, retrieves the appropriate Dinner object, checks to see if the logged-in user is currently in the list of users who have registered for it, and if not adds an RSVP object for them:

public class RSVPController : Controller {    DinnerRepository dinnerRepository = new DinnerRepository();    //    // AJAX: /Dinners/Register/1    [Authorize, AcceptVerbs(HttpVerbs.Post)]    public ActionResult Register(int id) {        Dinner dinner = dinnerRepository.GetDinner(id);        if (!dinner.IsUserRegistered(User.Identity.Name)) {            RSVP rsvp = new RSVP();            rsvp.AttendeeName = User.Identity.Name;            dinner.RSVPs.Add(rsvp);            dinnerRepository.Save();        }        return Content("Thanks - we'll see you there!");    }}

Notice, in the previous code, how we are returning a simple string as the output of the action method. We could have embedded this message within a view template — but since it is so small we'll just use the Content helper method on the controller base class and return a string message like that above.

Calling the Register Action Method Using AJAX

We'll use AJAX to invoke the Register action method from our Details view. Implementing this is pretty easy. First we'll add two script library references:

<script src="/Scripts/MicrosoftAjax.js" type=" text/javascript"></script><script src="/Scripts/MicrosoftMvcAjax.js" type=" text/javascript"></script>

The first library references the core ASP.NET AJAX client-side script library. This file is approximately 24k in size (compressed) and contains core client-side AJAX functionality. The second library contains utility functions that integrate with ASP.NET MVC's built-in AJAX helper methods (which we'll use shortly).

We can then update the view template code we added earlier so that, instead of outputing a "You are not registered for this event" message, we render a link that when pushed performs an AJAX call that invokes our Register action method on our RSVP controller and RSVPs the user:

<div id=" rsvpmsg"><% if (Request.IsAuthenticated) { %>    <% if (Model.IsUserRegistered(Context.User.Identity.Name)) { %>        <p>You are registered for this event!</p>    <% } else { %>        <%= Ajax.ActionLink("RSVP for this event",                            "Register", "RSVP",                            new { id=Model.DinnerID },                            new AjaxOptions { UpdateTargetId=" rsvpmsg" }) %>    <% } %><% } else { %>    <a href="/Account/Logon">Logon</a> to RSVP for this event.<% } %></div>

The Ajax.ActionLink helper method in the previous code is built into ASP.NET MVC and is similar to the Html.ActionLink helper method except that instead of performing a standard navigation, it makes an AJAX call to the action method. Above we are calling the "Register" action method on the "RSVP" controller and passing the DinnerID as the id parameter to it. The final AjaxOptions parameter we are passing indicates that we want to take the content returned from the action method and update the HTML <div> element on the page whose id is "rsvpmsg".

And now when a user browses to a dinner they aren't registered for yet, they'll see a link to RSVP for it (Figure 1-125).

Image from book
Figure 1-125

If they click the "RSVP for this event" link, they'll make an AJAX call to the Register action method on the RSVP controller, and when it completes they'll see an updated message like that in Figure 1-126.

Image from book
Figure 1-126

The network bandwidth and traffic involved when making this AJAX call is really lightweight. When the user clicks on the "RSVP for this event" link, a small HTTP POST network request is made to the /Dinners/Register/1 URL that looks like the following on the wire:

POST /Dinners/Register/49 HTTP/1.1X-Requested-With: XMLHttpRequestContent-Type: application/x-www-form-urlencoded; charset=utf-8Referer: http://localhost:8080/Dinners/Details/49

And the response from our Register action method is simply:

HTTP/1.1 200 OKContent-Type: text/html; charset=utf-8Content-Length: 29Thanks - we'll see you there!

This lightweight call is fast and will work even over a slow network.

Adding a jQuery Animation

The AJAX functionality we implemented works well and fast. Sometimes it can happen so fast, though, that a user might not notice that the RSVP link has been replaced with new text. To make the outcome a little more obvious, we can add a simple animation to draw attention to the updates message.

The default ASP.NET MVC project template includes jQuery — an excellent (and very popular) open source JavaScript library that is also supported by Microsoft. jQuery provides a number of features, including a nice HTML DOM selection and effects library.

To use jQuery, we'll first add a script reference to it. Because we are going to be using jQuery within a variety of places within our site, we'll add the script reference within our Site.master master page file so that all pages can use it.

<script src="/Scripts/jQuery-1.3.2.js" type=" text/javascript"></script>
Note 

Make sure you have installed the JavaScript IntelliSense hotfix for VS 2008 SP1 that enables richer intellisense support for JavaScript files (including jQuery). You can download it from: http://tinyurl.com/vs2008javascripthotfix

Code written using JQuery often uses a global $() JavaScript method that retrieves one or more HTML elements using a CSS selector. For example, $("#rsvpmsg") selects any HTML element with the ID of rsvpmsg, while $(".something") would select all elements with the "something" CSS class name.

You can also write more advanced queries like "return all of the checked radio buttons" using a selector query like: $("input[@type=radio][@checked]").

Once you've selected elements, you can call methods on them to take action, such as hiding them: $("#rsvpmsg").hide();

For our RSVP scenario, we'll define a simple JavaScript function named AnimateRSVPMessage that selects the "rsvpmsg" <div> and animates the size of its text content. The code below starts the text small and then causes it to increase over a 400 milliseconds timeframe:

<script type=" text/javascript">    function AnimateRSVPMessage() {        $("#rsvpmsg").animate({fontSize: "1.5em"}, 400);    }</script>

We can then wire up this JavaScript function to be called after our AJAX call successfully completes by passing its name to our Ajax.ActionLink helper method (via the AjaxOptions OnSuccess event property):

<%= Ajax.ActionLink( "RSVP for this event",                     "Register", "RSVP",                     new { id=Model.DinnerID },                     new AjaxOptions { UpdateTargetId=" rsvpmsg",                                 onSuccess=" AnimateRSVPMessage" }) %>

And now when the "RSVP for this event" link is clicked and our AJAX call completes successfully, the content message sent back will animate and grow large (Figure 1-127).

Image from book
Figure 1-127

In addition to providing an OnSuccess event, the AjaxOptions object exposes OnBegin, OnFailure, and OnComplete events that you can handle (along with a variety of other properties and useful options).

Cleanup — Refactor Out a RSVP Partial View

Our details view template is starting to get a little long, which over time will make it a little harder to understand. To help improve the code readability, let's finish up by creating a partial view — RSVPStatus.ascx — that encapsulates all of the RSVP view code for our Details page.

We can do this by right-clicking on the \Views\Dinners folder and then choosing the Add ð View menu command. We'll have it take a Dinner object as its strongly typed ViewModel. We can then copy/paste the RSVP content from our Details.aspx view into it.

Once we've done that, let's also create another partial view — EditAndDeleteLinks.ascx—that encapsulates our Edit and Delete link view code. We'll also have it take a Dinner object as its strongly typed ViewModel, and copy/paste the Edit and Delete logic from our Details.aspx view into it.

Our details view template can then just include two Html.RenderPartial method calls at the bottom:

<% Html.RenderPartial("RSVPStatus"); %><% Html.RenderPartial("EditAndDeleteLinks"); %>

This makes the code cleaner to read and maintain.

 

Integrating an AJAX Map

We'll now make our application a little more visually exciting by integrating AJAX mapping support. This will enable users who are creating, editing, or viewing dinners to see the location of the dinner graphically.

Creating a Map Partial View

We are going to use mapping functionality in several places within our application. To keep our code DRY, we'll encapsulate the common map functionality within a single partial template that we can reuse across multiple controller actions and views. We'll name this partial view map.ascx and create it within the \Views\Dinners directory.

We can create the map.ascx partial by right-clicking on the \Views\Dinners directory and choosing the Add ð View menu command. We'll name the view Map.ascx, check it as a partial view, and indicate that we are going to pass it a strongly typed Dinner model class (Figure 1-128):

Image from book
Figure 1-128

When we click the "Add" button our partial template will be created. We'll then update the Map.ascx file to have the following content:

<script src=" http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"type=" text/javascript"></script><script src="/Scripts/Map.js" type=" text/javascript"></script><div id=" theMap"></div><script type=" text/javascript">    $(document).ready(function() {        var latitude = <%=Model.Latitude %>;        var longitude = <%=Model.Longitude %>;        if ((latitude == 0) || (longitude == 0))            LoadMap();        else            LoadMap(latitude, longitude, mapLoaded);    });    function mapLoaded() {        var title = "<%= Html.Encode(Model.Title) %>";        var address = "<%= Html.Encode(Model.Address) %>";        LoadPin(center, title, address);        map.SetZoomLevel(14);    }</script>

The first <script> reference points to the Microsoft Virtual Earth 6.2 mapping library. The second <script> reference points to a map.js file that we will shortly create, which will encapsulate our common JavaScript mapping logic. The <div id=" theMap"> element is the HTML container that Virtual Earth will use to host the map.

We then have an embedded <script> block that contains two JavaScript functions specific to this view. The first function uses jQuery to wire up a function that executes when the page is ready to run client-side script. It calls a LoadMap helper function that we'll define within our Map.js script file to load the Virtual Earth map control. The second function is a callback event handler that adds a pin to the map that identifies a location.

Notice how we are using a server-side <%= %> block within the client-side script block to embed the latitude and longitude of the dinner we want to map into the JavaScript. This is a useful technique to output dynamic values that can be used by client-side script (without requiring a separate AJAX call back to the server to retrieve the values — which makes it faster). The <%= %> blocks will execute when the view is rendering on the server — and so the output of the HTML will just end up with embedded JavaScript values (for example: var latitude = 47.64312;).

Creating a Map.js Utility Library

Let's now create the Map.js file that we can use to encapsulate the JavaScript functionality for our map (and implement the LoadMap and LoadPin methods above). We can do this by right-clicking on the \Scripts directory within our project, and then choose the Add ð New Item menu command, select the JScript item, and name it Map.js.

Below is the JavaScript code we'll add to the Map.js file that will interact with Virtual Earth to display our map and add locations pins to it for our dinners:

var map = null;var points = [];var shapes = [];var center = null;function LoadMap(latitude, longitude, onMapLoaded) {    map = new VEMap(‘theMap');    options = new VEMapOptions();    options.EnableBirdseye = false;    // Makes the control bar less obtrusive.    map.SetDashboardSize(VEDashboardSize.Small);    if (onMapLoaded != null)        map.onLoadMap = onMapLoaded;    if (latitude != null && longitude != null) {        center = new VELatLong(latitude, longitude);    }    map.LoadMap(center, null, null, null, null, null, null, options);}function LoadPin(LL, name, description) {    var shape = new VEShape(VEShapeType.Pushpin, LL);    //Make a nice Pushpin shape with a title and description    shape.SetTitle("<span class=\"pinTitle\"> "+ escape(name) + "</span>");    if (description !== undefined) {        shape.SetDescription("<p class=\"pinDetails\">" +        escape(description) + "</p>");    }    map.AddShape(shape);    points.push(LL);    shapes.push(shape);}function FindAddressOnMap(where) {    var numberOfResults = 20;    var setBestMapView = true;    var showResults = true;    map.Find("", where, null, null, null,            numberOfResults, showResults, true, true,            setBestMapView, callbackForLocation);}function callbackForLocation(layer, resultsArray, places,            hasMore, VEErrorMessage) {    clearMap();    if (places == null)        return;    //Make a pushpin for each place we find    $.each(places, function(i, item) {        var description = "";        if (item.Description !== undefined) {            description = item.Description;        }        var LL = new VELatLong(item.LatLong.Latitude,                        item.LatLong.Longitude);        LoadPin(LL, item.Name, description);    });    //Make sure all pushpins are visible    if (points.length > 1) {        map.SetMapView(points);    }    //If we've found exactly one place, that's our address.    if (points.length === 1) {        $("#Latitude").val(points[0].Latitude);        $("#Longitude").val(points[0].Longitude);    }}function clearMap() {    map.Clear();    points = [];    shapes = [];}

Integrating the Map with Create and Edit Forms

We'll now integrate the Map support with our existing Create and Edit scenarios. The good news is that this is pretty easy to do, and doesn't require us to change any of our Controller code. Because our Create and Edit views share a common DinnerForm partial view used to implement the dinner form UI, we can add the map in one place and have both our Create and Edit scenarios use it.

All we need to do is to open the \Views\Dinners\DinnerForm.ascx partial view and update it to include our new map partial. Below is what the updated DinnerForm will look like once the map is added (the HTML form elements are omitted from the code snippet below for brevity):

<%= Html.ValidationSummary() %><% using (Html.BeginForm()) { %>    <fieldset>        <div id=" dinnerDiv">            <p>                   [HTML Form Elements Removed for Brevity]            </p>            <p>               <input type=" submit" value=" Save" />            </p>        </div>        <div id=" mapDiv">            <% Html.RenderPartial("Map", Model.Dinner); %>        </div>    </fieldset>    <script type=" text/javascript">        $(document).ready(function() {            $("#Address").blur(function(evt) {                $("#Latitude").val("");                $("#Longitude").val("");                var address = jQuery.trim($("#Address").val());                if (address.length < 1)                    return;                FindAddressOnMap(address);            });        });    </script><% } %>

The DinnerForm partial above takes an object of type DinnerFormViewModel as its model type (because it needs both a Dinner object and a SelectList to populate the drop-down list of countries).

Our map partial just needs an object of type Dinner as its model type, and so when we render the map partial we are passing just the Dinner sub-property of DinnerFormViewModel to it:

<% Html.RenderPartial("Map", Model.Dinner); %>

The JavaScript function we've added to the partial uses jQuery to attach a blur event to the Address HTML textbox. You've probably heard of focus events that fire when a user clicks or tabs into a textbox. The opposite is a blur event that fires when a user exits a textbox. The event handler in the previous code clears the latitude and longitude textbox values when this happens, and then plots the new address location on our map. A callback event handler that we defined within the map.js file will then update the longitude and latitude textboxes on our form using values returned by Virtual Earth based on the address we gave it.

And now when we run our application again and click the Host Dinner tab, we'll see a default map displayed along with our standard Dinner form elements (Figure 1-129).

Image from book
Figure 1-129

When we type in an address, and then tab away, the map will dynamically update to display the location, and our event handler will populate the latitude/longitude textboxes with the location values (Figure 1-130).

Image from book
Figure 1-130

If we save the new dinner and then open it again for editing, we'll find that the map location is displayed when the page loads (Figure 1-131).

Image from book
Figure 1-131

Every time the address field is changed, the map and the latitude/longitude coordinates will update.

Now that the map displays the dinner location, we can also change the Latitude and Longitude form fields from being visible textboxes to instead be hidden elements (since the map is automatically updating them each time an address is entered). To do this, we'll switch from using the Html.TextBox HTML helper to using the Html.Hidden helper method:

<p>    <%= Html.Hidden("Latitude", Model.Dinner.Latitude)%>    <%= Html.Hidden("Longitude", Model.Dinner.Longitude)%></p>

And now our forms are a little more user-friendly (Figure 1-132) and avoid displaying the raw latitude/longitude (while still storing them with each dinner in the database).

Image from book
Figure 1-132

Integrating the Map with the Details View

Now that we have the map integrated with our Create and Edit scenarios, let's also integrate it with our Details scenario. All we need to do is to call <% Html.RenderPartial("map"); %> within the Details view.

Below is what the source code to the complete Details view (with map integration) looks like:

<asp:Content ID=" Title" ContentPlaceHolderID=" TitleContent" runat=" server">    <%= Html.Encode(Model.Title) %></asp:Content><asp:Content ID=" details" ContentPlaceHolderID=" MainContent" runat=" server">    <div id=" dinnerDiv">        <h2><%= Html.Encode(Model.Title) %></h2>        <p>            <strong>When:</strong>            <%= Model.EventDate.ToShortDateString() %>            <strong>@</strong>            <%= Model.EventDate.ToShortTimeString() %>        </p>        <p>            <strong>Where:</strong>            <%= Html.Encode(Model.Address) %>,            <%= Html.Encode(Model.Country) %>        </p>         <p>            <strong>Description:</strong>            <%= Html.Encode(Model.Description) %>        </p>        <p>            <strong>Organizer:</strong>            <%= Html.Encode(Model.HostedBy) %>            (<%= Html.Encode(Model.ContactPhone) %>)        </p>        <% Html.RenderPartial("RSVPStatus"); %>        <% Html.RenderPartial("EditAndDeleteLinks"); %>    </div>    <div id=" mapDiv">        <% Html.RenderPartial("map"); %>    </div></asp:Content>

And now when a user navigates to a /Dinners/Details/[id] URL, they'll see details about the dinner, the location of the dinner on the map (complete with a pushpin that when hovered over displays the title of the dinner and the address of it), and have an AJAX link to RSVP for it (Figure 1-133).

Image from book
Figure 1-133

Implementing Location Search in Our Database and Repository

To finish off our AJAX implementation, let's add a map to the home page of the application that allows users to graphically search for dinners near them (Figure 1-134).

Image from book
Figure 1-134

We'll begin by implementing support within our database and data repository layer to efficiently perform a location-based radius search for dinners. We could use the new geospatial features of SQL 2008 (www.microsoft.com/sqlserver/2008/en/us/spatial-data.aspx) to implement this, or alternatively we can use a SQL function approach that Gary Dryden discussed in article here: www.codeproject.com/KB/cs/distancebetweenlocations.aspx and Rob Conery blogged about using with LINQ to SQL here: http://blog.wekeroad.com/2007/08/30/linq-and-geocoding/.

To implement this technique, we will open the Server Explorer within Visual Studio, select the NerdDinner database, and then right-click on the functions sub-node under it and choose to create a new Scalar-valued function (Figure 1-135).

Image from book
Figure 1-135

We'll then paste in the following DistanceBetween function:

CREATE FUNCTION [dbo].[DistanceBetween] (@Lat1 as real,                @Long1 as real, @Lat2 as real, @Long2 as real)RETURNS realASBEGINDECLARE @dLat1InRad as float(53);SET @dLat1InRad = @Lat1 * (PI()/180.0);DECLARE @dLong1InRad as float(53);SET @dLong1InRad = @Long1 * (PI()/180.0);DECLARE @dLat2InRad as float(53);SET @dLat2InRad = @Lat2 * (PI()/180.0);DECLARE @dLong2InRad as float(53);SET @dLong2InRad = @Long2 * (PI()/180.0);DECLARE @dLongitude as float(53);SET @dLongitude = @dLong2InRad - @dLong1InRad;DECLARE @dLatitude as float(53);SET @dLatitude = @dLat2InRad - @dLat1InRad;/* Intermediate result a. */DECLARE @a as float(53);SET @a = SQUARE (SIN (@dLatitude / 2.0)) + COS (@dLat1InRad)                 * COS (@dLat2InRad)                 * SQUARE(SIN (@dLongitude / 2.0));/* Intermediate result c (great circle distance in Radians). */DECLARE @c as real;SET @c = 2.0 * ATN2 (SQRT (@a), SQRT (1.0 - @a));DECLARE @kEarthRadius as real;/* SET kEarthRadius = 3956.0 miles */SET @kEarthRadius = 6376.5;        /* kms */DECLARE @dDistance as real;SET @dDistance = @kEarthRadius * @c;return (@dDistance);END

We'll then create a new table-valued function in SQL Server that we'll call NearestDinners (Figure 1-136):

Image from book
Figure 1-136

This NearestDinners table function uses the DistanceBetween helper function to return all dinners within 100 miles of the latitude and longitude we supply it:

CREATE FUNCTION [dbo].[NearestDinners]       (       @lat real,       @long real       )RETURNS TABLEAS       RETURN       SELECT Dinners.DinnerID       FROM Dinners       WHERE dbo.DistanceBetween(@lat, @long, Latitude, Longitude) <100

To call this function, we'll first open up the LINQ to SQL designer by double-clicking on the NerdDinner.dbml file within our \Models directory (Figure 1-137).

Image from book
Figure 1-137

We'll then drag the NearestDinners and DistanceBetween functions onto the LINQ to SQL designer, which will cause them to be added as methods on our LINQ to SQL NerdDinnerDataContext class (Figure 1-138).

Image from book
Figure 1-138

We can then expose a FindByLocation query method on our DinnerRepository class that uses the NearestDinner function to return upcoming dinners that are within 100 miles of the specified location:

public IQueryable<Dinner> FindByLocation(float latitude, float longitude) {   var dinners = from dinner in FindUpcomingDinners()                 join i in db.NearestDinners(latitude, longitude)                 on dinner.DinnerID equals i.DinnerID                 select dinner;    return dinners;}

Implementing a JSON-Based AJAX Search Action Method

We'll now implement a controller action method that takes advantage of the new FindByLocation repository method to return a list of Dinner data that can be used to populate a map. We'll have this action method return the Dinner data in a JSON (JavaScript Object Notation) format so that it can be easily manipulated using JavaScript on the client.

To implement this, we'll create a new SearchController class by right-clicking on the \Controllers directory and choosing the Add ð >Controller menu command. We'll then implement a SearchByLocation action method within the new SearchController class like the one that follows:

public class JsonDinner {    public int      DinnerID    { get; set; }    public string   Title       { get; set; }    public double   Latitude    { get; set; }    public double   Longitude   { get; set; }    public string   Description { get; set; }    public int      RSVPCount   { get; set; }}public class SearchController : Controller {    DinnerRepository dinnerRepository = new DinnerRepository();    //    // AJAX: /Search/SearchByLocation    [AcceptVerbs(HttpVerbs.Post)]    public ActionResult SearchByLocation(float longitude, float latitude) {        var dinners = dinnerRepository.FindByLocation(latitude, longitude);        var jsonDinners = from dinner in dinners                          select new JsonDinner {                              DinnerID = dinner.DinnerID,                              Latitude = dinner.Latitude,                              Longitude = dinner.Longitude,                              Title = dinner.Title,                              Description = dinner.Description,                              RSVPCount = dinner.RSVPs.Count                          };        return Json(jsonDinners.ToList());    }}

The SearchController's SearchByLocation action method internally calls the FindByLocation method on DinnerRespository to get a list of nearby dinners. Rather than return the Dinner objects directly to the client, though, it instead returns JsonDinner objects. The JsonDinner class exposes a subset of Dinner properties (for example: for security reasons it doesn't disclose the names of the people who have RSVP'ed for a dinner). It also includes an RSVPCount property that doesn't exist in Dinner — and that is dynamically calculated by counting the number of RSVP objects associated with a particular dinner.

We are then using the Json helper method on the Controller base class to return the sequence of dinners using a JSON-based wire format. JSON is a standard text format for representing simple data structures. The following is an example of what a JSON-formatted list of two JsonDinner objects looks like when returned from our action method:

 [{"DinnerID":53," Title":" Dinner with the Family"," Latitude":47.64312," Longitude":-122.130609," Description":" Fun dinner"," RSVPCount":2},{"DinnerID":54," Title":" Another Dinner"," Latitude":47.632546," Longitude":-122.21201," Description":" Dinner with Friends"," RSVPCount":3}]

Calling the JSON-Based AJAX Method Using jQuery

We are now ready to update the home page of the NerdDinner application to use the SearchController's SearchByLocation action method. To do this, we'll open the /Views/Home/Index.aspx view template and update it to have a textbox, search button, our map, and a <div> element named dinnerList:

<h2>Find a Dinner</h2><div id=" mapDivLeft">    <div id=" searchBox">        Enter your location: <%= Html.TextBox("Location") %>        <input id=" search" type=" submit" value=" Search" />    </div>    <div id=" theMap">    </div></div><div id=" mapDivRight">    <div id=" dinnerList"></div></div>

We can then add two JavaScript functions to the page:

<script type=" text/javascript">    $(document).ready(function() {        LoadMap();    });    $("#search").click(function(evt) {        var where = jQuery.trim($("#Location").val());        if (where.length < 1)            return;        FindDinnersGivenLocation(where);    });</script>

The first JavaScript function loads the map when the page first loads. The second JavaScript function wires up a JavaScript click event handler on the search button. When the button is pressed, it calls the FindDinnersGivenLocation JavaScript function which we'll add to our Map.js file:

function FindDinnersGivenLocation(where) {    map.Find("", where, null, null, null, null, null, false,      null, null, callbackUpdateMapDinners);}

This FindDinnersGivenLocation function calls map.Find on the Virtual Earth Control to center it on the entered location. When the Virtual Earth map service returns, the map.Find method invokes the callbackUpdateMapDinners callback method we passed it as the final argument.

The callbackUpdateMapDinners method is where the real work is done. It uses jQuery's $.post helper method to perform an AJAX call to our SearchController's SearchByLocation action method — passing it the latitude and longitude of the newly centered map. It defines an inline function that will be called when the $.post helper method completes, and the JSON-formatted dinner results returned from the SearchByLocation action method will be passed it using a variable called dinners. It then does a foreach over each returned dinner, and uses the dinner's latitude and longitude and other properties to add a new pin on the map. It also adds a dinner entry to the HTML list of dinners to the right of the map. It then wires up a hover event for both the pushpins and the HTML list so that details about the dinner are displayed when a user hovers over them:

function callbackUpdateMapDinners(layer, resultsArray,    places, hasMore, VEErrorMessage) {    $("#dinnerList").empty();    clearMap();    var center = map.GetCenter();    $.post("/Search/SearchByLocation", { latitude: center.Latitude,                                         longitude: center.Longitude },    function(dinners) {        $.each(dinners, function(i, dinner) {            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/Details/‘ + dinner.DinnerID + ‘">'                        + dinner.Title + ‘</a>',                        "<p>" + dinner.Description + "</p>" + RsvpMessage);            //Add a dinner to the <ul> dinnerList on the right            $(‘#dinnerList').append($(‘<li/>')                            .attr("class", "dinnerItem")                            .append($(‘<a/>').attr("href",                                      "/Dinners/Details/"+ dinner.DinnerID)                            .html(dinner.Title))                            .append("("+RsvpMessage+")"));    });    // 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.    $(".dinnerItem").each(function(i, dinner) {        $(dinner).hover(            function() { map.ShowInfoBox(shapes[i]); },            function() { map.HideInfoBox(shapes[i]); }        );    });}, "json");

And now when we run the application and visit the home page, we'll be presented with a map. When we enter the name of a city the map will display the upcoming dinners near it (Figure 1-139).

Image from book
Figure 1-139

Hovering over a dinner will display details about it (Figure 1-140).

Image from book
Figure 1-140

Clicking the Dinner title either in the bubble or on the right-hand side in the HTML list will navigate us to the dinner — which we can then optionally RSVP for (Figure 1-141).

Image from book
Figure 1-141
posted on 2010-06-23 15:43  Yan-Feng  阅读(1116)  评论(0编辑  收藏  举报