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

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

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

By Scott Mitchell


Introduction


Last week's article, Implementing the Store Locator Application Using ASP.NET MVC (Part 1), started a two-part article series that walked through converting my ASP.NET store locator application from WebForms to ASP.NET MVC. Last week's article stepped through the first tasks in porting the store locator application to ASP.NET MVC, including: creating the new project; copying over stylesheets, the database, scripts, and other shared content from the WebForms application; building the HomeController; and coding the Index and StoreLocator actions and views.

Recall that the StoreLocator action and view prompts the user to enter an address for which to find nearby stores. On form submission, the action interfaces with the Google Maps API's geocoding service to determine if the entered address corresponds to known latitude and longitude coordinates. If so, the user is redirected to the StoreLocatorResults action (which we create in this article) that displays the nearby stores in both a grid and as markers on a map. Unlike the StoreLocator action created in Part 1, the StoreLocatorResults action uses a more intricate model and a strongly-typed view. 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. It also presumes that you have read and worked through Part 1 of this article series. If you have not yet read these articles and downloaded and tested the code, please take time to do that before continuing on with this article.

Part 1 presented a series of steps to perform to convert the existing store locator application to ASP.NET MVC, and ended with "Step 4: Creating the StoreLocator Action and View." This article picks up where Part 1 left off, starting with Step 5.

Step 5: Creating the Object Model Using LINQ To SQL


The store locator application includes a database with a single table (Stores) that contains one record for each store location. In the WebForms application this database is queried from the ShowStoreLocations.aspx page, which is where the user is redirected after they've entered a valid address from which to find nearby stores. The ShowStoreLocations.aspx page uses a SqlDataSource control to retrieve those stores within (roughly) 15 miles of the address entered by the user and these nearby stores are both listed in a grid and plotted on a map.

One of the design goals of ASP.NET MVC is to offer a clean separation of concerns. The user interface - the view, in MVC parlance - should not include data access or business logic code. With ASP.NET WebForms is it all too easy to mesh together the user interface, data access, and business logic; using a SqlDataSource in the ASP.NET page is one such example of this mixing of concerns.

To help maintain separation of concerns, let's use LINQ to SQL, which is a simple, straightforward object-relational mapping tool from Microsoft that builds an object model that represents the data in a database. This article does not delve into LINQ to SQL in depth and assumes the reader has some familiarity with it. If this is not the case, check out Scott Guthrie's multi-part Using LINQ to SQL tutorials.

To create the LINQ to SQL classes, right-click on the Models folder and choose to add a new item. Navigate to the Data templates and add a LINQ to SQL Classes file named StoreLocations.dbml.

Add a LINQ to SQL Classes file named StoreLocations.dbml.

Once the StoreLocations.dbml file has been added and opened, go to the Server Explorer window, expand the StoreLocations.mdf database node's Tables folder and then drag the Stores table onto the LINQ to SQL designer. This creates a new object named Store that contains properties that map to the database table's columns as well as a StoreLocationsDataContext class that we will use later to programmatically retrieve those stores with 15 miles (or so) of the user-entered address.

Save your changes and close the StoreLocations.dbml file. If you go to the Class View you should see a new namespace in your project (Web.Models) with two new classes - Store and StoreLocationsDataContext. The Store class represents the Stores database table and has properties that correspond to the table's columns (StoreNumber, Address, City, Latitude, Longitude, and so forth). The StoreLocatorDataContext class is used to get store information from the underlying database.

Step 6: A First Stab at the StoreLocatorResults Action and View


When a user visits the StoreLocator action (which we created in Part 1) and enters a valid address, she is whisked to the StoreLocatorResults action, which we have yet to create. Let's do that now.

Add a method named StoreLocatorResults that returns an ActionResult object to the HomeController class. Recall that when the user enters a valid address they are redirected to this action with the address they entered passed through the querystring, like so: /Home/StoreLocatorResults?Address=address. Update the StoreLocatorResults action method to have a string input parameter named address; ASP.NET MVC will automatically assign that input parameter the value of the Address querystring field. At this point the method definition should look like the following:

public ActionResult StoreLocatorResults(string address)
{
   ...
}

The StoreLocatorResults action is responsible for determining what stores are nearby the specified address. To accomplish this we need to start by determining the latitude and longitude of the address, which we can do using the same technique as in Part 1, namely calling the GoogleMapsAPIHelpersCS.GetGeocodingSearchResults method and passing in the address. Once armed with the latitude and longitude coordinates we can determine the nearby stores by creating an instance of the StoreLocatorDataContext and then returning all records from its Stores collection whose latitude and longitude is less than 0.25 units away from the latitude and longitude of the address entered by the user. (As discussed in the WebForms article, this will return stores within, roughly, a 15 mile diameter of the user-entered address.)

The set of nearby stores needs to be available to the view so that these stores can be displayed in a grid and on a map. ASP.NET MVC provides two mechanisms for building the model: through the loosely-typed ViewData collection and through strongly-typed models. We already saw how to use the loosely-typed ViewData collection when we created the StoreLocator action and view back in Part 1. To use a strongly-typed model we need to do two things:

  1. Pass the model to the view using the syntax, return View(model), and
  2. Specify the model's data type from the Inherits attribute in the view's @Page directive.
The following code performs the necessary tasks of determining the latitude and longitude for the user-entered address, retrieving nearby stores, and passing the model to the view:

// Get the lat/long info about the address
var results = GoogleMapsAPIHelpersCS.GetGeocodingSearchResults(address);

// Determine the lat & long parameters
var lat = Convert.ToDecimal(results.Element("result").Element("geometry").Element("location").Element("lat").Value);
var lng = Convert.ToDecimal(results.Element("result").Element("geometry").Element("location").Element("lng").Value);


// Get those locations near the store
var context = new StoreLocationsDataContext();
var nearbyStores = from store in context.Stores
                   where Math.Abs(store.Latitude - lat) < 0.25M &&
                         Math.Abs(store.Longitude - lng) < 0.25M
                   select store;

return View(nearbyStores);

The next step is to create the StoreLocatorResults's view. Right-click on the StoreLocatorResults method name in the HomeController class and choose Add View. From the Add View dialog box, check the "Create a strongly-typed view" checkbox and choose the Web.Models.Store type from the drop-down list. Also, select the List option from the "View content" drop-down list. Finally, click the Add button to create the strongly-typed view.

Add a strongly-typed view using the Web.Models.Store data type.

Note the new StoreLocatorResults.aspx view's @Page directive's Inherits attribute, which lets the view know that its model will be of a particular type (in this case, an enumerable collection of Store objects):

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<Web.Models.Store>>" %>

Choosing the List template for the view causes Visual Studio to add markup that displays the contents of the model in a grid, with a column for each property. (There are also Edit, Details, and Delete links that link to non-existent actions.) If you visit the application through a browser, navigate to the StoreLocator action, and enter a valid address, you will be sent to the StoreLocatorResults action and shown a grid of the nearby stores. The following screen shot shows this grid when entering the address "San Diego".

A grid of nearby stores is displayed.

At this point you could customize the markup to remove the Edit, Details, and Delete links and to reformat the grid to make the output look more like it did in the original article series. But rather than doing that, let's take a step back and question whether we want to use a collection of Store objects as our model in the first place. While the Store class contains much of the information we want to display in the results page - the address, for example - it is missing other bits of important information, most notably the distance from the user-address entered. As discussed in the original article series, the distance between each store and the user-entered address can be computed using the Pythagorean theorem. This computation could certainly be performed directly in the view; however, ASP.NET MVC views should be simple. Ideally, the distance between each store location and the user-entered address would be part of the model itself and would not need to be determined by the view.

Rather than using a collection of Store objects as our model, let's instead take the time to create a more finely-tuned model for this view, one that will intuitively know the distance between the store and the user-entered address and not require any computation within the view.

Step 7: Creating the StoreLocator Model


Start by creating a new class in the Models folder named NearbyStoreLocation. This class will represent a store location nearby a particular address. In short, this class needs to offer the same functionality as the existing Store class, but with the addition of the user-entered address's latitude and longitude coordinates and a property to determine the distance between the store and the user-entered address.

Here's my version of the NearbyStoreLocation class, which I have extending the existing Store class. Note the read-only DistanceFromAddress property, which uses the Pythagorean theorem to determine the distance between the store's latitude and longitude and the supplied address's latitude and longitude. Additionally, a read-only DistanceFromAddressDisplay property provides a formatted version of this distance for use in the view.

public class NearbyStoreLocation : Store
{
   public decimal AddressLatitude { get; set; }
   public decimal AddressLongitude { get; set; }

   public decimal DistanceFromAddress
   {
      get
      {
         return Convert.ToDecimal(
                  Math.Sqrt(
                        Math.Pow(Convert.ToDouble(this.Latitude - this.AddressLatitude), 2.0)
                           +
                        Math.Pow(Convert.ToDouble(this.Longitude - this.AddressLongitude), 2.0)
                  )
                  * 62.1371192
               );
      }
   }
      
   public string DistanceFromAddressDisplay
   {
      get
      {
         return this.DistanceFromAddress.ToString("0.00 miles");
      }
   }
}

Step 8: Updating the StoreLocatorResults Action and View


With this model in place, return to the StoreLocatorResults action and replace the current nearbyStores query with the following:

// Get those locations near the store
var context = new StoreLocationsDataContext();
var nearbyStores = from store in context.Stores
               where Math.Abs(store.Latitude - lat) < 0.25M &&
                     Math.Abs(store.Longitude - lng) < 0.25M
               select new NearbyStoreLocation()
               {
                  StoreNumber = store.StoreNumber,
                  Address = store.Address,
                  City = store.City,
                  Region = store.Region,
                  CountryCode = store.CountryCode,
                  PostalCode = store.PostalCode,
                  Latitude = store.Latitude,
                  Longitude = store.Longitude,
                  AddressLatitude = lat,
                  AddressLongitude = lng
               };

// Order the results from nearest to farthest
var nearbySortedStores = nearbyStores.ToList().OrderBy(s => s.DistanceFromAddress).ToList();

return return View(nearbySortedStores);

The highlighted portions indicate what has changed. Previously, the query returned each Store object that was within a particular distance from the user-entered address. The updated query above creates a new NearbyStoreLocation object for each matching store. Note that the created NearbyStoreLocation objects have their AddressLatitude and AddressLongitude properties set to the latitude and longitude of the user-entered address (lat and lng, respectively).

In addition to the modified query, I also have added an OrderBy clause so that the stores are ordered from the nearest to the farthest away. These results are stored in a new query, nearbySortedStores, which is what is passed to the view as the model.

Next, modify the view's Inherits attribute, replacing IEnumerable<Web.Models.Store> with List<Web.Models.NearbyStoreLocation>. Nuke the existing markup and replace it with the following, which shows the results in a grid similar in appearance to the one created in the WebForms application:

<table cellspacing="0" cellpadding="5" rules="all" class="searchResults">
   <tr>
      <th>Store #</th>
      <th>Distance</th>
      <th>Address</th>
   </tr>
   <% foreach (var store in Model) { %>
      <tr>
         <td><%: store.StoreNumber%></td>
         <td><%: store.DistanceFromAddressDisplay%></td>
         <td>
            <%: store.Address%><br />
            <%: store.City%>, <%: store.Region%><br />
            <%: store.CountryCode%> <%: store.PostalCode%>
         </td>
      </tr>
   <% } %>
</table>

The following screen shot shows the updated view when searching for stores near "San Diego":

The grid shows those stores near San Diego.

Step 9: Adding a the Map to the StoreLocatorResults View


In addition to a grid of nearby stores, store locator applications often include a map with markers showing the nearby locations. The earlier WebForms store locator application used Google's free Google Maps API to display a map and place markers showing the location of the nearby stores. This involved adding a bit of JavaScript to the page, calling the init_map JavaScript function in the ~/Scripts/GoogleMapHelpers.js file, and passing it the latitude and longitude of the address entered along with arrays containing information about the markers to plot on the map.

In the WebForms application, the JavaScript for the map and markers was constructed in server-side code and then emitted to the screen via a call to the ClientScript.RegisterStartupScript method. With ASP.NET MVC, we construct this JavaScript and call it directly from the view.

First things first, we need to reference the Google Maps API JavaScript content and our own GoogleMapHelpers.js file from the <head> portion of the web page. This can be done by using the Content control for the HeadContent ContentPlaceHolder:

<asp:Content ID="head" ContentPlaceHolderID="HeadContent" runat="server">
   <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
   <script type="text/javascript" src='<%=Page.ResolveClientUrl("~/Scripts/GoogleMapHelpers.js")%>'></script>
</asp:Content>

Next, we need to define the map canvas, which is where the map will appear on the web page. Add this above the <table> that lists the nearby locations.

<div id="map_canvas" class="map-area"<>/div>

We also need to build up the JavaScript arrays that specify the position and titles of the markers (and their popup windows). To accomplish this create two List<string>s named locations and infoWindowContents. As we loop through each nearby store we need to add that store's information to the arrays, which the code below does.

<%
   var locations = new List<string>();
   var infoWindowContents = new List<string>();


   foreach (var store in Model) {
      locations.Add(string.Format(
         @"{{
               title: ""Store #{0}"",
               position: new google.maps.LatLng({1}, {2})
         }}",
         store.StoreNumber,
         store.Latitude,
         store.Longitude       );

      infoWindowContents.Add(string.Format(
         @"{{
            content: ""<div class=\""infoWindow\""><b>Store #{0}</b><br />{1}<br />{2}, {3} {4}</div>""
         }}",
         store.StoreNumber,
         store.Address,
         store.City,
         store.Region,
         store.PostalCode)
      );
%>
   <tr>
      <td><%: store.StoreNumber%></td>
      <td><%: store.DistanceFromAddressDisplay%></td>
      <td>
         <%: store.Address%><br />
         <%: store.City%>, <%: store.Region%><br />
         <%: store.CountryCode%> <%: store.PostalCode%>
      </td>
   </tr>
<%
   }
%>

The highlighted content shows what has been added. Through each iteration of the foreach loop information about the current location is added to the locations and infoWindowContents arrays. After the nearby stores have been enumerated, the List<string>s are converted into a single string of JavaScript code, which is then passed as one of the input parameters to the init_map JavaScript function. In addition to the location and popup window information, the init_map function is also passed the latitude and longitude of the user-entered address.

<%
   var locationsJson = "[" + string.Join(",", locations.ToArray()) + "]";
   var overlayContentsJson = "[" + string.Join(",", infoWindowContents.ToArray()) + "]";
%>

<script type="text/javascript">
   init_map('map_canvas', <%: Model[0].AddressLatitude %>, <%: Model[0].AddressLongitude %>, 13, <%=locationsJson %>, <%=overlayContentsJson %>);
</script>

The screen shot below shows the current iteration of the store locator page when searching for stores near "San Diego."

The map and grid show those stores near San Diego.

Step 10: Tidying Up and Final Steps


At this point we have a functioning store locator application in ASP.NET MVC, although there are still a couple of rough edges. Rather than walking through each of these in fine detail, I'll instead just point them out and leave them as features that you can implement on your own. (The download available at the end of this article includes the fully completed application with all of these final touches.)

As things stand now, an error will occur if a user enters an address for which there are no nearby stores (such as "Chicago"). In the event that there are no nearby stores, the user should see a suitable message.

As the screen shot above shows, each store location on the map is displayed using the default marker, a red circle with a black dot. In the WebForms store locator application we looked at using the GeneratedImage control to dynamically create a custom marker with the number 1, 2, 3, and so on, and then showed the matching icon down in the grid. In this way, users could quickly see what stores in the grid corresponded to what stores on the map. Adding this functionality in ASP.NET MVC isn't difficult, but requires a bit of a discussion and the addition of a new routing rule. Rather than detail those changes here, please refer to my blog entry, Using the GeneratedImage Control in ASP.NET MVC. The download at the end of this article includes the configuration changes and the needed code.

I also added a "Directions" link to both the grid and the info windows that popup when you click a marker on the map. Clicking the "Directions" link opens a new browser window that loads Google Maps, showing the directions from the user-entered address to the selected store.

Happy Programming!

  • By Scott Mitchell


    Attachments:


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


  • Building a Store Locator ASP.NET Application Using Google Maps API
  • Implementing the Store Locator Application Using ASP.NET MVC (Part 1)
  • Google Maps API
  • Dynamically Generating and Caching Images in ASP.NET with the GeneratedImage Control
  • Using the GeneratedImage Control in ASP.NET MVC


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