Asp.Net MVC programming advice. Toodles, Evan Nagle.
Jul
09

New to The Big Boy MVC Series?
Read the series from its humble beginnings.


Whoo. It’s been awhile. Apologies for that. I’ve been looking for a new place to live in Honolulu, and the whole process has turned out to be an uphill battle (thanks for asking). It appears that real estate brokers don’t really like it too much when you fill in the “Sex: ___________” blank with “OFFENDER“.

Some of you might have died between Part 22 and Part 23. That’s okay. For those of you who are still sentient and coherent and death-free, you’ll remember that, in Part 22, we set up our ViewModel layer, and we mapped our FoodAdd ViewModel to our Model Food class (which was autogenerated by Linq 2 SQL). Remember? If not, go ahead and take a stroll down memory lane.

Today, we’re going to handle our first Http Post. Since, like cheese, you’ve aged (and learned some stuff) as we’ve trekked along on this journey of ours, I’m going to provide more code and less banter in this post. Or, maybe not.

Step 1: Make some small changes to your dbml file and your database. I destroyed the States table, I cleaned up the column names, I added permalink columns to each table, and I added a Points table. These small changes will help us keep our downstream code so fresh and so clean. Also, if you’re skillfully building your application, these minor tweaks should be super easy to implement. If they’re not, you’re probably suffering from a good ol’ case of spaghetti fever. Lasagna:

BFD e1278652165473 The Big Boy MVC Series    Part 23: Baby Got Postbacks, a Preamble

Step 2: Design a permalink generator. All the cool kids are doing it. And, like a cool kid, we want to be able to feed our permalink generator a stupid string like “Hub’s Hot Dog H3X##~~0rFest”, and, in turn, we want our permalink generator to spit out a clean url-friendly string like “hubs-hot-dog-h3x0rfest”. Easy enough, eh? Heck, if it’s that easy maybe you should try to create this permalink generator all by your big-boy self, eh? Your version might suck much less than mine. And, if it does, please e-mail it to me. Or, if it doesn’t, feel free to steal mine.

Already finished? Well, okay then. Let’s compare biceps.

Here are my unit tests:


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Big.Fat.Dish.Models;

namespace Big.Fat.Dish.Tests.Models
{
    [TestClass]
    public class PermalinkTest
    {
        [TestMethod]
        public void Models_Permalink_For_Trims_Spaces()
        {
            string title = "    hello-there     ";
            string permalink = Permalink.For(title);
            Assert.AreEqual("hello-there", permalink);
        }

        [TestMethod]
        public void Models_Permalink_For_Delimits()
        {
            string title = "here is my title";
            string permalink = Permalink.For(title);
            Assert.AreEqual("here-is-my-title", permalink);
        }

        [TestMethod]
        public void Models_Permalink_For_Converts_To_LowerInvariant()
        {
            string title = "HeRe";
            string permalink = Permalink.For(title);
            Assert.AreEqual("here", permalink);
        }

        [TestMethod]
        public void Models_Permalink_For_Trims_Inner()
        {
            string title = "here  is    my      title";
            string permalink = Permalink.For(title);
            Assert.AreEqual("here-is-my-title", permalink);
        }

        [TestMethod]
        public void Models_Permalink_For_Removes_NonAlphanumeric_Chars()
        {
            string title = "Mr. Jim Lives in Room 1,209----ok?";
            string permalink = Permalink.For(title);
            Assert.AreEqual("mr-jim-lives-in-room-1209-ok", permalink);
        }

        [TestMethod, ExpectedException(typeof(ArgumentNullException))]
        public void Models_Permalink_For_Throws_Null_Argument_Error_When_Null()
        {
            string error = Permalink.For(null);
        }

        [TestMethod, ExpectedException(typeof(ArgumentNullException))]
        public void Models_Permalink_For_Throws_Null_Argument_Error_When_Empty()
        {
            string error = Permalink.For(string.Empty);
        }

        [TestMethod]
        public void Models_Permalink_For_Generates_Permalinks_For_NonalphaNames()
        {
            string name = Permalink.For("###", ifEmptyUse:"permalink");

            Assert.IsNotNull(name);
            Assert.AreEqual(name, "permalink");
        }
    }
}

And here is my Permalink Generator:


using System;
using System.Text.RegularExpressions;

namespace Big.Fat.Dish.Models
{
    public static class Permalink
    {
        static Regex nonAlphanumeric = new Regex(@"[^\w\d\s\-]+", RegexOptions.Compiled);
        static Regex spacesAndDashes = new Regex(@"[\s\-]+", RegexOptions.Compiled);

        public static string For(string value, string delimiter = "-", string ifEmptyUse = "#")
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentNullException("value");
            }

            string alpha = nonAlphanumeric.Replace(value, string.Empty).Trim().ToLowerInvariant();
            string delimitedAlpha = spacesAndDashes.Replace(alpha, delimiter);

            if (string.IsNullOrWhiteSpace(delimitedAlpha))
            {
                return ifEmptyUse;
            }
            else
            {
                return delimitedAlpha;
            }
        }
    }
}

Step 3: Design a server-side geocoder. A whaaaa?

This is a bit trickier. In fact, this might be the “trickiest” piece of code that we’ve written thus far, or that we’ll ever write in this measly little pea-brained application of ours. Hence, to accomplish this feat of strange brilliance, you’ll need to have a basic understanding of JSON Serialization. And you’ll also need to get pretty cozy with the Google Maps API. So, get cracking.

We’re going to make a WebRequest to Google. When we make our web request, we’re proverbially asking Google if we can come over, chat about soccer, and make sweet sweet love on the beige ottoman. e.g.:

Dolphin Mating Ritual 1 The Big Boy MVC Series    Part 23: Baby Got Postbacks, a Preamble

I should stop here for a second and disclose an important legal disclaimer: since we’re grabbing and parsing geocoding data from grandfather Google, we can only use that data for Google Mapping purposes. And, as the TOS explicitly states, we’re legally obliged to regularly update this data such that the data, at any given point, is “temporary”. That should hopefully keep our project on the borderline of “non-prostitutory” and “legit”–and, thus, with any luck, we won’t find ourselves embroiled in a David/Goliath battle with the ol’ internet giant. If we do, we can always run. Like a girl. Or, we can just go ahead and start using Bing Maps.

Like always, let’s start with some unit tests. When building our geocoding unit tests, we need to make sure that we don’t make a real WebRequest. Google (long wind, sleeping giant) wouldn’t like that. So, instead, we’ll use RhinoMocks to mock out a fake WebRequest, which we’ll use to return a (fake) serializied JSON string that our geocoder can fiddle with.


using System.IO;
using System.Web.Script.Serialization;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;
using Big.Fat.Dish.Models;

namespace Big.Fat.Dish.Tests.WebRequests.Geocoding
{
    [TestClass]
    public class GeocoderTest
    {
        MockRepository mocks;
        IGeocoder geocoder;
        IWebRequestWrap request;
        JavaScriptSerializer serializer;

        const string anyString = "pointless";
        GeoAddress deserializedAddress = new GeoAddress
        {
            Status = new Status
            {
                Code = GeoResponse.Success,
                Request = "geocode"
            },
            Placemark = new Placemark[]
            {
                new Placemark
                {
                    AddressDetails = new AddressDetails
                    {
                        Country = new Country
                        {
                            AdministrativeArea = new AdministrativeArea
                            {
                                AdministrativeAreaName = "WA",
                                SubAdministrativeArea = new SubAdministrativeArea
                                {
                                    Locality = new Locality
                                    {
                                        LocalityName = "Seattle",
                                        Thoroughfare = new Thoroughfare
                                        {
                                            ThoroughfareName = "111 Seattle WA 98116"
                                        },
                                        PostalCode = new PostalCode
                                        {
                                            PostalCodeNumber = "98116"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    Point = new GeoPoint
                    {
                        Coordinates = new float[]
                        {
                            100,
                            -100
                        }
                    }
                }
            }
        };

        [TestInitialize]
        public void Init()
        {
            mocks = new MockRepository();
            request = mocks.DynamicMock<IWebRequestWrap>();
            serializer = new JavaScriptSerializer();

            string serializedAddress = serializer.Serialize(deserializedAddress);
            request.Stub(m => m.GetResponseAsString(Arg<string>.Is.Anything)).Return(serializedAddress);

            geocoder = new Geocoder(request);
            mocks.ReplayAll();
        }

        [TestCleanup]
        public void TestCleanup()
        {
            mocks.VerifyAll();
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_Line1()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual("111 Seattle WA 98116", returnedAddress.Line1);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Line2_Is_Null()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.IsNull(returnedAddress.Line2);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_City()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual("Seattle", returnedAddress.City);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_State()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual("WA", returnedAddress.State);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_PostalCode()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual("98116", returnedAddress.PostalCode);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_Point_Longitude()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual(100, returnedAddress.Point.Longitude);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_Point_Lattitude()
        {
            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.AreEqual(-100, returnedAddress.Point.Latitude);
        }

        [TestMethod]
        public void WebRequests_Geocoder_GetAddressFromString_Returns_Null_On_Error()
        {
            deserializedAddress.Status.Code = GeoResponse.BadKey;
            Init();

            Address returnedAddress = geocoder.GetAddressFromString(anyString);
            Assert.IsNull(returnedAddress);
        }
    }
}

Alrighty. Let’s start building! First, we need to create a wrapper for our WebRequest. In a similar flash of passion, we spent some time a few days ago wrapping up our frisky DataContext. For now, with our WebRequestWrap class, we really only need to expose one method. He’s a lonely fellow:


namespace Big.Fat.Dish.WebRequests
{
    public interface IWebRequestWrap
    {
        string GetResponseAsString(string uri);
    }
}

And our implementation:


using System.Net;
using System.IO;

namespace Big.Fat.Dish.WebRequests
{
    public class WebRequestWrap : IWebRequestWrap
    {
        public string GetResponseAsString(string uri)
        {
            var request = WebRequest.Create(uri);
            var response = request.GetResponse();
            string responseAsString;

            using (var stream = response.GetResponseStream())
            using (var reader = new StreamReader(stream))
            {
                responseAsString = reader.ReadToEnd();
            }

            return responseAsString;
        }
    }
}

Next, let’s create our Geocoder class. Here’s the interface:


using Big.Fat.Dish.WebRequests.Geocoding.Deserialized;
using Big.Fat.Dish.Models;

namespace Big.Fat.Dish.WebRequests.Geocoding
{
    public interface IGeocoder
    {
        Address GetAddressFromString(string address);
    }
}

And the Geocoder class:


using System.Web.Script.Serialization;
using Big.Fat.Dish.WebRequests.Geocoding.Deserialized;
using Big.Fat.Dish.WebRequests;
using Big.Fat.Dish.Models;

namespace Big.Fat.Dish.WebRequests.Geocoding
{
    public class Geocoder : IGeocoder
    {
        const string MapKey = "GetYourOwnMapKey,Buddy";
        const string MapUrl = "http://maps.google.com/maps/geo?q={0}&output=json&oe=utf8&sensor=false&key={1}";

        IWebRequestWrap webRequest;

        public Geocoder() : this(new WebRequestWrap()) { }
        public Geocoder(IWebRequestWrap webRequest)
        {
            this.webRequest = webRequest;
        }

        GeoAddress Geocode(string streetAddress)
        {
            //handle null address
            if (string.IsNullOrEmpty(streetAddress)) return null;

            //get json response
            string url = string.Format(MapUrl, streetAddress, MapKey);
            string response = webRequest.GetResponseAsString(url);

            //serialize response
            try
            {
                var serializer = new JavaScriptSerializer();
                var jsonAddress = serializer.Deserialize(response);

                return jsonAddress;
            }
            catch
            {
                return null;
            }
        }

        public Address GetAddressFromString(string streetAddress)
        {
            var geo = Geocode(streetAddress);
            if (geo == null || geo.Status == null || geo.Status.Code != GeoResponse.Success)
            {
                return null;
            }

            try
            {
                var placemark = geo.Placemark[0];
                var adminArea = placemark.AddressDetails.Country.AdministrativeArea;
                var locality = adminArea.SubAdministrativeArea.Locality;
                var coords = placemark.Point.Coordinates;

                return new Address
                {
                    Line1 = locality.Thoroughfare.ThoroughfareName,
                    Line2 = null,
                    City = locality.LocalityName,
                    State = adminArea.AdministrativeAreaName,
                    PostalCode = locality.PostalCode.PostalCodeNumber,
                    Point = new Point
                    {
                        Longitude = coords[0],
                        Latitude = coords[1]
                    }
                };
            }
            catch
            {
                return null;
            }
        }
    }
}

The only missing piece of this big, bad puzzle: the GeoAddress class (& crap), which our deserializer fills to the brim with spicy geocoding data. I’ve included all of the classes below, so hopefully you get the idea. Don’t blame me for the class-count here; blame Google:

GeoAddress


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class GeoAddress
    {
        public Placemark[] Placemark { get; set; }
        public Status Status { get; set; }
    }
}

Placemark


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class Placemark
    {
        public GeoPoint Point { get; set; }
        public AddressDetails AddressDetails { get; set; }
    }
}

Status


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class Status
    {
        public GeoResponse Code { get; set; }
        public string Request { get; set; }
    }
}

GeoPoint


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class GeoPoint
    {
        public float[] Coordinates { get; set; }
    }
}

AddressDetails


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class AddressDetails
    {
        public Country Country { get; set; }
    }
}

Country


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class Country
    {
        public AdministrativeArea AdministrativeArea { get; set; }
    }
}

AdministrativeArea


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class AdministrativeArea
    {
        public string AdministrativeAreaName { get; set; }
        public SubAdministrativeArea SubAdministrativeArea { get; set; }
    }
}

SubAdministrativeArea


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class SubAdministrativeArea
    {
        public Locality Locality { get; set; }
    }
}

Locality


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class Locality
    {
        public string LocalityName { get; set; }
        public Thoroughfare Thoroughfare { get; set; }
        public PostalCode PostalCode { get; set; }
    }
}

Thoroughfare


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class Thoroughfare
    {
        public string ThoroughfareName { get; set; }
    }
}

PostalCode


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public class PostalCode
    {
        public string PostalCodeNumber { get; set; }
    }
}

GeoResponse


namespace Big.Fat.Dish.WebRequests.Geocoding.Deserialized
{
    public enum GeoResponse
    {
        Success = 200,
        BadRequest = 400,
        ServerError = 500,
        MissingQuery = 601,
        MissingAddress = 601,
        UnknownAddress = 602 | 604,
        UnavailableAddress = 603,
        BadKey = 610,
        TooManyQueries = 620
    }
}

Step 4: Run the tests. Feel the Purple Rain. Our geocoder works. Our permalink generator works. Our hearts are almost completely mended (you may not realize it yet):

Tests Passed e1278672229946 The Big Boy MVC Series    Part 23: Baby Got Postbacks, a Preamble


Move on to Part 24: Dear EditorTemplates, Are We Done Posting Yet?.

Read More

You can leave a response, or trackback from your own site.

7 Responses to “The Big Boy MVC Series — Part 23: Baby Got Postbacks, a Preamble”

 
  1. codinghobbit says:

    You may want to have a look as NBuilder (or alike) to create your test objects within your tests..

  2. Evan says:

    Hey codinghobbit,

    Thanks for the suggestion!

    I actually don’t mind NBuilder and/or AutoFixture. In this case, I wanted to write out (or, eh, design) the GeoAddress in my test class before creating the actual GeoAddress class.

    Though it’s more code for me to write, I actually prefer it. Because, ultimately, if a new developer took a peak at this test class, he’d know everything he’d need to know about the structure of the deserialized address class. Hence, the test not only verifies that my units are functional, it also serves as a set of nice, readable, reconfigurable design specifications. For me (or for any other poor sucker who has to fix my inevitable mistakes), that’s quite a gift.

    Make sense? Am I crazy? Would love more input.

  3. bully says:

    Just a quickie – how do you handle permalinks that aren’t unique. For example, two completely different posts entitled “billy-the-kid”. Will the permalink code need to check the database for existing posts of the same name?

  4. Evan says:

    Hey there Bully,

    I’m actually just using the permalinks for SEO purposes. Like Stack Overflow. Notice that this url:

    http://stackoverflow.com/questions/3305904/my-answer-get-converted-to-communiti-wiki

    Points to the same page as this url:

    http://stackoverflow.com/questions/3305904/how-to-kill-everyone-that-you-love-and-not-get-caught

    The Id is used to retrieve all of the necessary data, so the duplicate permalinks aren’t an issue.

    Hope that makes sense. If you DID want to access data via the permalink, then yes, you’d definitely have to verify that the permalink is unique.

  5. codinghobbit says:

    @Evan..

    I never thought of it that way before. You some good points, more work upfront but easier for others to maintain.

    “Am I crazy” – aren’t we all??

  6. Ryan Heath says:

    Perhaps you could replace accented characters with their non-accented equivalent.

    Something like

    static Regex aAccents = new Regex(@”[ÀÁÂÃÄÅàáâãäå]“, RegexOptions.Compiled);
    static Regex eAccents = new Regex(@”[ÈÉÊËèéêë]“, RegexOptions.Compiled);
    // etc for other chars with accents

    var aAccentLess = aAccents.Replace(value, “a”);
    var aeAccentLess = aAccents.Replace(aAccentLess , “e”);
    // etc for other chars with accents

    It’s a pain there is no regex shortcut for these characters.

    Then again, english speakers will use accented chars less more likely than the other European languages speakers ;)

    // Ryan

    PS Enjoying your MVC serie :)

 

Leave a Reply