Implementing the Store Locator Application Using ASP.NET MVC (Part 1)By Scott Mitchell
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!
|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 -
Views, and so on - as well as an appropriately configured
Global.asax file that defines the default route.
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:
StoreLocations.mdfdatabase file (and the
StoreLocations.ldflog file) in the
App_Datafolder need to be copied over to an
App_Datafolder 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
CustomStyles.css. In the WebForms application these files were located in the
Stylesfolder. For the ASP.NET MVC application they need to be placed in the
GoogleMapsAPIHelpersCS.csfile, which is located in the
App_Code/CSCodefolder 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_Codefolder; instead, create a new folder in the ASP.NET MVC application named
HelperClassesand then copy the
GoogleMapsAPIHelpersCS.csfile into this new folder.
- Copy the
GoogleMapHelpers.jsfile from the WebForms application's
Scriptsfolder into the ASP.NET MVC application's
Scriptsfolder. Feel free to delete the jQuery and Microsoft ASP.NET Ajax-related files in the ASP.NET MVC application's
Scriptsfolder, as we won't be using them.
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\Sharedfolder, so expand the
Viewsfolder, right-click on the
Sharedfolder, and add a new MVC 2 View Master Page named
Site.master. Next, open the
Site.mastermaster 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.mastermaster 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
<link> elements in the
<head> element reference the CSS files
as if they were in a subfolder named
Styles. However, these files, relative to the master page, are two directories up and in the
href attributes in the
<link> elements need to be updated. Also, add a
<link> element for the
(also in the
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
HeadContent to match the naming convention used with the
After making these changes your ASP.NET MVC application's master page'ssection should similar to the following:
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
Default.aspx page is the application's homepage, whereas the
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),
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
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.aspxprompted 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.aspxwith the address information passed through the querystring.
ShowStoreLocations.aspxthen 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
StoreLocatorResults, which function
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.
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 (
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 they same controller and action will execute. The
HomeController will also be where we add our
StoreLocatorResults actions, meaning this functionality will be accessible via
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
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.
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
<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
Html.ActionLink method call above creates the HTML to render a hyperlink to the
StoreLocator action (
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
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
statements, one that points to the
Index and the other that points to the
Step 4: Creating the
StoreLocator Action and View
HomeControllerclass created, we're ready to add our
StoreLocatoractions. (There will actually be two
StoreLocatoractions - 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
StoreLocatorResultsaction 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
HomeController class named
StoreLocator that returns a view. After making these changes your ASP.NET MVC application's
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
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!"
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
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
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
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:
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
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
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
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 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
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
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
else statements in the
action, with the just-added code highlighted.
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
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).
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
ViewData collection contains a
NoResults element and, consequently, displays a message alerting the user. (Note: I made some changes to
input-validation-error CSS class in
Site.css to get the formatting you see in the screen shot below.)
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
The above markup/code starts by determining if the
ViewData collection contains an element named
Matches. If so, some markup is emitted and then
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.
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
HomeControllerclass and implemented both the
StoreLocatoractions and views. All that remains is to implement the
StoreLocatorResultsaction, which is responsible for displaying the nearby stores in a grid and on a map. We'll tackle this in an upcoming Part 2.