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!
| 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.
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.mdfdatabase file (and theStoreLocations.ldflog file) in theApp_Datafolder need to be copied over to anApp_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
sinorcaish-screen.cssandCustomStyles.css. In the WebForms application these files were located in theStylesfolder. For the ASP.NET MVC application they need to be placed in theContentfolder. - The
GoogleMapsAPIHelpersCS.csfile, which is located in theApp_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 anApp_Codefolder; instead, create a new folder in the ASP.NET MVC application namedHelperClassesand then copy theGoogleMapsAPIHelpersCS.csfile into this new folder. - Copy the
GoogleMapHelpers.jsfile from the WebForms application'sScriptsfolder into the ASP.NET MVC application'sScriptsfolder. Feel free to delete the jQuery and Microsoft ASP.NET Ajax-related files in the ASP.NET MVC application'sScriptsfolder, 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\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">
|
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.
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.
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>
|
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>
|
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
public class HomeController : Controller
|
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()) { %>
|
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 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
|
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]
|
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)
|
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) { %>
|
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.)
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:
<%
|
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.
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.
Happy Programming!
Attachments: