When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article
Jobs

ASP ASP.NET ASP FAQs Message Board Feedback ASP Jobs
 
Print this Page!
Published: Wednesday, August 18, 2010

Implementing the Store Locator Application Using ASP.NET MVC (Part 1)

By Scott Mitchell


Introduction


Back in May 2010 I wrote a three-part article series titled Building a Store Locator ASP.NET Application Using Google Maps API, which showed how to build a simple store locator application using ASP.NET and the Google Maps API. The application consisted of two ASP.NET pages. In the first page, the user was prompted to enter an address, city, or postal code (screen shot). On postback, the user-entered address was fed into the Google Maps API's geocoding service to determine whether the address, as entered, corresponded to known latitude and longitude coordinates. If it did, the user was redirected to the second page with the address information passed through the querystring. This page then queried the database to find nearby stores and listed them in a grid and as markers on a map (screen shot).

Since the WebForms store locator application was published, several readers have emailed me to ask for an ASP.NET MVC version. I recently decided to port the existing WebForms application to ASP.NET MVC. This article, the first in a two-part series, walks through creating the ASP.NET MVC version of the store locator application and pinpoints some of the more interesting and challenging aspects. This article examines creating the ASP.NET MVC application and building the functionality for the user to enter an address from which to find nearby stores. Part 2 will examine how to show a grid and map of the nearby stores. Read on to learn more!

- continued -

Before We Get Started...
This article assumes you have already read, downloaded, and used the original store locator application introduced in the Building a Store Locator ASP.NET Application Using Google Maps API article series. If you have not yet read those articles and downloaded and tested the code, please take time to do that before continuing on with this article.

This article gives step-by-step instructions on building the store locator application using ASP.NET MVC; if you like, you can follow along from your computer as you read through the article. This ASP.NET MVC application is available for download at the end of this article and was created using C#, Visual Studio 2010, ASP.NET 4, and ASP.NET MVC 2.

A key advantage of ASP.NET MVC over the WebForms model is that ASP.NET MVC applications naturally lend themselves to test-driven development (TDD). When creating ASP.NET MVC applications it is good practice to create a Unit Test project and to practice TDD. However, for this application I bypassed that step. I considered creating unit tests, but decided to pass on them for two reasons: first, I wanted the port to ASP.NET MVC to be as similar to the original WebForms version as possible, and the WebForms version did not include any tests; second, I was curious how long it would take to convert the existing application to ASP.NET MVC and was more interested in finding the quickest time, not the time it would take to do it "right." Long story short, realize the code presented in this article is meant to illustrate some of the challenges you might face when porting a WebForms application to ASP.NET MVC, as well as to highlight some of the key differences between the two models. It is not intended as a best practices guide.

Step 1: Getting Started


This article provides step-by-step instructions on how I created the ASP.NET MVC version of the store locator application and is designed so that you can follow along and create the same ASP.NET MVC application with me from the ground up. In porting the application to ASP.NET MVC I used C# as the programming language as well as the latest versions of Visual Studio and ASP.NET MVC at the time of writing, namely Visual Studio 2010 and ASP.NET MVC 2. So fire up Visual Studio 2010 and let's get started!

To start, create a new ASP.NET MVC 2 Empty Web Application project. Doing so will create a new project with the pertinent ASP.NET MVC folders - Content, Controllers, Models, Views, and so on - as well as an appropriately configured Global.asax file that defines the default route.

Create a new ASP.NET MVC 2 Empty Web Application.

There are a number of files from the original WebForms store locator application that will be needed in this ASP.NET MVC version, so let's add those to the project now. (You can get these files by downloading the WebForms store locator application at http://aspnet.4guysfromrolla.com/code/GoogleMapsDemo3.zip.) The files that need to be included in the ASP.NET MVC project include:

  • The StoreLocations.mdf database file (and the StoreLocations.ldf log file) in the App_Data folder need to be copied over to an App_Data folder in the ASP.NET MVC application. This database contains the table (Stores) that has a record for each of the store locations.
  • The CSS files sinorcaish-screen.css and CustomStyles.css. In the WebForms application these files were located in the Styles folder. For the ASP.NET MVC application they need to be placed in the Content folder.
  • The GoogleMapsAPIHelpersCS.cs file, which is located in the App_Code/CSCode folder in the WebForms application, needs to be included in the ASP.NET MVC application. Because the ASP.NET MVC application is a Web Application Project you should not create an App_Code folder; instead, create a new folder in the ASP.NET MVC application named HelperClasses and then copy the GoogleMapsAPIHelpersCS.cs file into this new folder.
  • Copy the GoogleMapHelpers.js file from the WebForms application's Scripts folder into the ASP.NET MVC application's Scripts folder. Feel free to delete the jQuery and Microsoft ASP.NET Ajax-related files in the ASP.NET MVC application's Scripts folder, as we won't be using them.
After adding these existing files to the ASP.NET MVC application your Solution Explorer should look similar to the following:

The Solution Explorer lists the newly added files.

Step 2: Creating the Master Page


With the necessary files from the original store locator application copied into our ASP.NET MVC application, the next step is to create a master page for the ASP.NET MVC application. Master pages belong in the Views\Shared folder, so expand the Views folder, right-click on the Shared folder, and add a new MVC 2 View Master Page named Site.master. Next, open the Site.master master page from the WebForms store locator application and select all of the text in the page except for the <%@ Master %> directive at the very top. Copy this content to the clipboard and then paste it into the ASP.NET MVC application's Site.master master page, overwriting all of the content except for its <%@ Master %> directive.

The master page's content imported from the WebForms application is good to go, for the most part. There are a few areas we'll need to fix up, though. First up, note that the <link> elements in the <head> element reference the CSS files sinorcaish-screen.css and CustomStyles.css as if they were in a subfolder named Styles. However, these files, relative to the master page, are two directories up and in the Content folder, so the href attributes in the <link> elements need to be updated. Also, add a <link> element for the Site.css file (also in the Content folder).

Another change that's worth making is replacing the <title> attribute with a ContentPlaceHolder control so that each content page can define the title declaratively. Replace the existing text within the <title> element - "Untitled Page" - with a ContentPlaceHolder named TitleContent. Also, let's change the head ContentPlaceHolder's ID from head to HeadContent to match the naming convention used with the TitleContent ContentPlaceHolder.

After making these changes your ASP.NET MVC application's master page's section should similar to the following:

<head runat="server">
   <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>

   <asp:ContentPlaceHolder id="head" runat="server">
   </asp:ContentPlaceHolder>

   <link href="../../Content/sinorcaish-screen.css" rel="stylesheet" type="text/css" />
   <link href="../../Content/CustomStyles.css" rel="stylesheet" type="text/css" />
   <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>

Next, we need to get rid of the WebForm and other Web controls within the master page. Delete the server-side <form> element's opening and closing tags and the ScriptManager control.

Finally, the links in the left sidebar need to be updated. Right now there are three links to web pages in the application - one to Default.aspx, one to FindAStore.aspx, and one to FindAStoreCS.aspx. The Default.aspx page is the application's homepage, whereas the FindAStore.aspx and FindAStoreCS.aspx pages are the starting points for the store locator application (in VB and C#, respectively) where the visitor will enter the address for which she wants to see nearby stores. ASP.NET MVC applications URLs take the form Controller/Action (by default), rather than PageName.aspx. At this point we haven't yet decided the name of the controller or actions we'll be using. Once we decide upon these names we'll need to return to the master page and update these URLs. For now, feel free to leave them as-is.

One final change before we're done with the master page - change the ID of the ContentPlaceHolder control in the Main Content section of the page from ContentPlaceHolder1 to MainContent.

Step 3: Creating the Home Controller and the Index Action and View


In an ASP.NET MVC application, incoming requests are handled by a controller, which is responsible for building the model and choosing the view to render the user interface. In the WebForms application there were two pertinent pages, FindAStore.aspx and ShowStoreLocations.aspx. FindAStore.aspx prompted the user to enter an address. Upon entering an address that corresponded to a known latitude and longitude pair, the user was redirected to ShowStoreLocations.aspx with the address information passed through the querystring. ShowStoreLocations.aspx then displayed a grid of nearby stores and a map of the entered address with nearby stores rendered as markers on the map.

Let's keep this pattern in use - namely using two "pages" to implement the store locator application. Of course, in ASP.NET MVC we don't have pages, but rather actions. Let's create a controller named HomeController that will have actions named StoreLocator and StoreLocatorResults, which function like the FindAStore.aspx and ShowStoreLocations.aspx pages in the WebForms application.

To start off, we need a controller, which is a class that contains the actions. Right-click on the Controllers folder and choose to add a new controller. This displays the Add Controller dialog box, where you can specify the controller's name and whether Visual Studio should automatically create action methods for create, update, delete, and details scenarios. Name it HomeController and do not have it auto-generate any actions.

Add a new controller named HomeController.

A controller is composed of actions, which are public methods that return an object of type ActionResult. When a visitor reaches an ASP.NET MVC application the URL determines which controller and which action is executed. (The mapping from URL patterns to which action is executed is orchestrated by ASP.NET's Routing framework and ASP.NET MVC's MapRoute extension method, two topics that are beyond the scope of this article.)

For example, the controller class that was just created already has an action defined (Index). If a user were to visit www.yoursite.com/Home/Index then this action would be executed. What's more, in Global.asax the routing rule specifies default values of "Home" and "Index" for the controller and action portions of the URL, meaning that if someone visits www.yoursite.com/Home or www.yoursite.com they same controller and action will execute. The HomeController will also be where we add our StoreLocator and StoreLocatorResults actions, meaning this functionality will be accessible via www.yoursite.com/Home/StoreLocator and www.yoursite.com/Home/StoreLocatorResults, respectively.

Actions typically have an associated view, which is responsible for generating the user interface for the request. By default, views are implemented as .aspx files in the Views folder. Creating a view is simple enough - just right-click on the action's method name in the controller's class file and choose the Add View option from the context menu. This brings up the Add View dialog box (shown below) where you can name the view and choose its master page, among other things.

Let's take a moment to create a view for the Index action. Right-click on the Index method name in the HomeController.cs class file and choose Add View. When the Add View dialog box appears, click OK.

Add a view for the HomeController's Index action.

Adding the view creates a new file in the Views\Home folder named Index.aspx. Let's keep this "page" simple, with nothing but a link to the "page" where the user will enter an address. Recall that the link to enter the address will be www.yoursite.com/Home/StoreLocator. Consequently, you can create an <a> element like so: <a href="/Home/StoreLocator">Enter an address!</a>. Alternatively, you can use ASP.NET MVC's Html.ActionLink method to generate a hyperlink to a specific action. This server-side method takes input parameters such as the link text and the name of the action to link to, and returns a string of HTML. Using the Html.ActionLink method is preferred to hard-coding your hyperlinks because it is more resilient to changes in the controller names.

Add the following markup to the MainContent Content control in Index.aspx:

<h2>Welcome to the Store Locator (MVC)!</h2>
<p>
   <%: Html.ActionLink("Find a store near you!", "StoreLocator") %>
</p>

The Html.ActionLink method call above creates the HTML to render a hyperlink to the StoreLocator action (Home/StoreLocator) with the link text, "Find a store near you!" (Specifically, it generates the HTML <a href="/Home/StoreLocator">Find a store near you!</a>.) To emit the HTML returned by this server-side method we surround it with the <%: ... %> tags which tells ASP.NET to output the result of the code within and to HTML-encode it. For more on how the <%: ... %> tags work, see New <%: %> Syntax for HTML Encoding Output in ASP.NET 4 (and ASP.NET MVC 2).

Now would be a good time to return to the master page and update the links in the left sidebar. I suggest replacing the hyperlinks with two Html.ActionLink statements, one that points to the Index and the other that points to the StoreLocator action:

<ul>
   <li><%: Html.ActionLink("Home", "Index") %></li>
   <li><%: Html.ActionLink("Find a Store!", "StoreLocator") %></li>
</ul>

Step 4: Creating the StoreLocator Action and View


With the HomeController class created, we're ready to add our StoreLocator actions. (There will actually be two StoreLocator actions - more on that in a moment.) Recall that the intent of this page is to prompt the user to enter an address. Once the address is entered we need to invoke the Google Maps API's geocoding service to determine if the address is a valid address - that is, one that has a known latitude and longitude pair. If so, great, we can send the user onto the StoreLocatorResults action to display the nearby stores. If not, that means one of two things: either the address is completely unknown or there may be multiple matching addresses. For example, entering an address of "Springfield" is ambiguous because Google doesn't know if you mean Springfield, Missouri; Springfield, Illinois; Springfield, Ohio; or some other Springfield.

But first things first - let's create a simple action that returns a view with the user interface for entering the address information. Start by adding a new action to the HomeController class named StoreLocator that returns a view. After making these changes your ASP.NET MVC application's master page's section should similar to the following:

public class HomeController : Controller
{
   ...

   //
   // GET: /Home/StoreLocator
   public ActionResult StoreLocator()
   {
      return View();
   }
}

Next, add a view for the StoreLocator action. In the view we need to create a form that, when submitted, will do a postback, which is when we'll determine whether the entered address was valid or not. Inside the form we need a textbox for the user to enter the address and a button that, when clicked, will submit the form. With WebForms you would simply use a TextBox control and a Button control, but with ASP.NET MVC you do not have those controls at your disposal. Instead, you are on the hook for writing the necessary HTML. To simplify this process, ASP.NET MVC offers a number of helpers methods available through the Html object. As the following content shows, the Html class's BeginForm and TextBox methods are used to generate the HTML for a <form> and for a textbox (<input type="text" ... />). Put the following markup into the Content control for the MainContent ContentPlaceHolder.

<% using (Html.BeginForm()) { %>
   <p>
      <b>Your Address:</b>
      <%: Html.TextBox("address")%>
      
      <input type="submit" value="Search!" />
      
      <br />
      <i>Example: San Diego, CA</i> or <i>90505</i> or <i>600 Ash St., San Diego</i>
   </p>
<% } %>

If you visit this page through a browser (www.yoursite.com/Home/StoreLocator) you should see a user interface that prompts you for an address with a button titled "Search!"

The user interface for entering an address.

Clicking the "Search!" button submits the form, causing the browser to re-request www.yoursite.com/Home/StoreLocator and sending the address entered by the user back to the server. This, again, causes the HomeController class's StoreLocator action to execute, but this time we want to take the user's address and determine if it's valid. To accomplish this we could expand our existing action to handle the postback logic, but a cleaner approach is to create another StoreLocator action that is configured to execute only when there is a postback. What's more, we can define the parameters we expect to be passed back on form submission as input parameters to this action method.

The following code snippet achieves this end. Note the addition of the [HttpPost] attribute - this is what tells ASP.NET MVC to only run this action when the form is posted back. Also note the action's input parameter, address, of type string. This input parameter is automatically assigned the value the user entered into the address textbox.

public class HomeController : Controller
{
   ...

   //
   // POST: /Home/StoreLocator
   [HttpPost]
   public ActionResult StoreLocator(string address)
   {
      ...
   }
}

Much of the code for this action can be taken from the code created in the WebForms version. Here is the majority of the code, with a few places that still need filling out:

[HttpPost]
public ActionResult StoreLocator(string address)
{
   // Make sure we have an address
   if (string.IsNullOrEmpty(address))
      return View();

   var results = GoogleMapsAPIHelpersCS.GetGeocodingSearchResults(address);

   var resultCount = results.Elements("result").Count();

   if (resultCount == 0)
   {
      // No matching address found!
      return View();
   }
   else if (resultCount == 1)
   {
      // Got back exactly one result, show it!
      return RedirectToAction("StoreLocatorResults", new { Address = results.Element("result").Element("formatted_address").Value });
   }
   else
   {
      // Got back multiple results - We need to ask the user which address they mean to use...
      return View();
   }
}

Note that the method starts by asking, "Did the user supply an address?" If not, the view is returned, so the user sees the same page as before. Presuming an address was supplied, the code calls the GoogleMapsAPIHelpersCS class's GetGeocodingSearchResults method, which interacts with the Google Maps API's geocoding service to determine if the address is valid. If the address is valid there will be one result returned from the GetGeocodingSearchResults method, namely the address that was passed into the method. If there were no results then we need to tell the user their address could not be found. Likewise, if there are multiple results that means that there was some ambiguity, in which case we want to show the user various options to choose from.

Right now in the cases when there are zero results or multiple results we return the view. This has the effect of just reloading the page and is, obviously, no help to the user. We'll come back to this momentarily to see how we can send information to the view that zero or multiple results were found so that the view can display more relevant information. But before we get there, let's look at what we're doing in the case of one result. Note that this returns a RedirectToAction object, which, simply put, tells the browser, "Hey, go over here." It's akin to a Response.Redirect, but instead of specifying a URL to redirect to we specify an action. In this case we say, "Go to the StoreLocatorResults action and pass along the formatted address." The net effect is that the browser will be told to request www.yoursite.com/Home/StoreLocatorResults?Address=address. This will result in a 404 for the time being, as there is no StoreLocatorResults action defined in the HomeController class. We'll create this action in Part 2, which will be responsible for taking that address and showing nearby store locations in both a grid and on a map.

Now that we have the case where there is precisely one result out of the way (with the actual functionality of showing the nearby stores saved for Part 2), let's return to how we're going to deal with the case when there's zero or multiple results. In short, when there are zero or multiple results we need some way for the action to tell the view this information. Moreover, if there are multiple results the action needs to tell the view what results were found so that they may be displayed.

With MVC, the model is used to manage information. The controller is tasked with constructing the model, which the view can use to generate its output. ASP.NET MVC provides two mechanisms for building the model: through the loosely-typed ViewData collection and through strongly-typed models. The StoreLocatorResults action, which we'll examine in Part 2 of this series, uses a strongly-typed model. For the StoreLocator action, let's use the ViewData collection.

The ViewData collection is a dictionary who items are indexed by a string. The syntax for accessing or reading values from ViewData is the same as with session variables - you just use ViewData["someName"] = someValue to add an item to the ViewData collection and ViewData["someName"] to read that value back out. But unlike session, which is specific to a user and lives across requests, the items you put into the ViewData collection are specific to a request. Think of ViewData as this big ol' bucket that you can throw things into from the controller that the view can then rummage through.

Let's update our code in the StoreLocator action to use the ViewData collection. In the case when there are no matching addresses found we'll add a flag to the ViewData collection that tells the view as much. In the case of multiple results, we'll take the formatted addresses from all of the potential matches and put that in the ViewData collection. The following code shows the update if and else statements in the StoreLocator action, with the just-added code highlighted.

if (resultCount == 0)
{
   // No matching address found!
   ViewData["NoResults"] = true;

   return View();
}
else if (resultCount == 1)
{
   // Got back exactly one result, show it!
   ...
}
else
{
   // Got back multiple results - We need to ask the user which address they mean to use...
   var matches = from result in results.Elements("result")
               let formatted_address = result.Element("formatted_address").Value
               select formatted_address;

   ViewData["Matches"] = matches;


   return View();
}

Note that in the case of zero results we add a ViewData element named NoResults with a value of true. In the case of multiple addresses we get back just the collection of formatted addresses (which are each a string) and stick that in the ViewData collection with the name Matches.

The last part of the puzzle is to update the view so that it displays appropriate information based on whether there are no results or multiple results. First, the markup for handling no results (which I have placed beneath the textbox and button, but could really go anywhere in the view).

<% if (ViewData["NoResults"] != null) { %>
      <div class="input-validation-error">
         The address you entered is not known or understood. Try simplifying the address, or enter just a city, region, or postal code...
      </div>
<% } %>

The above markup contains a mix of server-side code and HTML. We start by determining if there's an item in the ViewData collection named NoResults. If so, that implies that no results were found, in which case we emit the HTML to display a message that explains that the entered address was not found.

To see this in action, enter a bogus address, like "asdf". As the following screen shot shows, on postback the geocoding service reports no matching addresses, which causes the action to add the NoResults value to the ViewData collection and to return the view. When the view is rendered, it notes that the ViewData collection contains a NoResults element and, consequently, displays a message alerting the user. (Note: I made some changes to the input-validation-error CSS class in Site.css to get the formatting you see in the screen shot below.)

A message is displayed explaining that there were no results found.

If there are multiple addresses found, we want to display a list of these possible addresses, with each one linking to the SearchLocatorResults action passing along the address through the querystring. For example, say the user searches on "Springfield". Google's geocoding service will return a collection of potential addresses, including: "Springfield, MO, USA"; "Springfield, IL, USA,"; and "Springfield, OH, USA", among others. We want to let the user click one of these and be taken to the results "page," just as if they had typed in the formatted address initially.

To achieve this, the view needs to loop through the results and, for each one, add a link to the StoreLocator action, passing the address through the querystring just like we did in the StoreLocator action when there was precisely one result from the geocoding service. The following markup and code accomplishes this:

<%
var possibleMatches = ViewData["Matches"] as IEnumerable<string>;
if (possibleMatches != null) { %>
   <div style="padding-left: 25px; margin-top: 10px;">
      <b>Did you mean...</b>
      <ol>
         <% foreach (var match in possibleMatches) { %>
            <li><%: Html.ActionLink(match, "StoreLocatorResults", new { Address = match }) %></li>
         <% } %>
      </ol>
   </div>
<% } %>

The above markup/code starts by determining if the ViewData collection contains an element named Matches. If so, some markup is emitted and then a foreach loop is used to enumerate the possible addresses. For each address, a link is generated (using Html.ActionLink) to the StoreLocatorResults action, passing the address through the querystring. To test out this functionality, enter an address like "Springfield". You should see a bulleted list of possible address matches. Clicking one of them takes you to the StoreLocatorResults action, passing the address through the querystring.

A list of possible addresses are displayed.

Looking Forward...


At this point we have succeeded in porting the first part of the WebForms store locator application to ASP.NET MVC. In particular, we created the ASP.NET MVC web application, added a master page, created the HomeController class and implemented both the Index and StoreLocator actions and views. All that remains is to implement the StoreLocatorResults action, which is responsible for displaying the nearby stores in a grid and on a map. We'll tackle this in an upcoming Part 2.

  • Read Part 2!

    Happy Programming!

  • By Scott Mitchell


    Attachments:


  • Download the ASP.NET MVC version of the Store Locator application
  • Further Readings:


  • Implementing the Store Locator Application Using ASP.NET MVC (Part 2)
  • Building a Store Locator ASP.NET Application Using Google Maps API
  • The Google Maps API
  • The ASP.NET MVC Official Site


  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article