Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / Azure

Job Match

5.00/5 (4 votes)
31 Jul 2013CPOL10 min read 13.9K  
Job Match helps job seekers find employment with the right company

This article is an entry in our DnB Developer Challenge. Articles in this sub-section are not required to be full articles so care should be taken when voting.

Introduction

The Government may claim that the Great Recession is technically over, but millions of Americans still face a tough job market. Sadly, bad economic times have a disproportionate effect on our most vulnerable groups. Statistics show that African Americans, Hispanics, women, veterans, and other historically disadvantaged groups have higher rates of unemployment in this economy than the general population. With Job Match, I hope to give these job seekers an extra tool for finding employment.

The basic goal of Job Match is to help people find companies that are more likely to hire them and provide a good environment for success. An African American student graduating from college can look up companies in his area that are notable for hiring larger numbers of minority workers. A mother who has spent the last several years raising her children, now ready to rejoin the workface can find a business in her area that is owned by other women and may provide better opportunities for her career advancement. Veterans especially, can find it difficult to re-adjust to civilian life after coming home from war. Seeking out employment is just one of the many challenges they face on the home front.

The Application

Job Match Site

For this contest I went with an ASP.NET MVC4 website hosted on Azure which uses a SQL Database backend. The site relies heavily on LINQ, Ajax, Jquery, and Bing Maps.

Users start off on the home page where they can select their state(s), or optionally allow the browser to capture their location.

Then users click on one of the four main groups, Minority, Women, Veterans, and Disadvantaged (Handicapped, disabled, etc...). From there, the user is taken to the main results page that shows a simple list of the companies matching that category for the state(s) selected.

Check marking these companies will bring them into view on the Bing Maps control on the right, along with showing some extra details. After selecting between 2 and 4 companies, a user can hit the Compare button which pop ups a table comparing quick facts on the company (size, distance from user, etc...).

Clicking on the name of a company in the list will bring the user to the Details page for that company. Aside from seeing more detailed company information, the user can see which other categories the company falls under. Clicking on the available categories will bring up further information pertinent to that category.

What if the user wants to keep track of companies they are interested in? Notice the "Log in" link on the top right. If the user wishes, they can create an account on the site for the purposes of marking companies as favorites to look up at a latter date. Registering and logging in couldn't be any easier. Rather than having to remember a username and password for yet another website, the user can choose to authenticate against the popular accounts they already own. Clicking Facebook, Microsoft, or Google will let them register and login with the credentials from these services.


Using the code

First I'll go over Bing Maps. This was my first foray into Bing Maps and it was a lot easier than I expected. Simple add the Bing Map script your page and call some code like below

<script src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0" type="text/javascript" charset="UTF-8"></script>

<script>

//Create your map
var map = new Microsoft.Maps.Map(mapDiv, { credentials: "your_key" });

//Move your map to a new location (assume lat and long are passed in as doubles)
var loc = new Microsoft.Maps.Location(Latitude, Longitude);
map.setView({
            center: loc,
            zoom: 10
        });

//Add a pin (clearing any other pins first)
var pin = new Microsoft.Maps.Pushpin(loc);
map.entities.clear();
        map.entities.push(pin);

</script>

This is the general gist of how I create Bing Map controls on my pages and move it around. I use Jquery Ajax calls to bring in the Lat Long points from the server, which I'll show later down in the code section. When the user first starts up our site, if they elect to Locate themselves by hitting the "Get Location" button, here is how we get their location to feed into the map.

To get the location of the user, I used the HTM5 Geocode feature that any HTML5 capable browser should support. Its very easy.

function GetLocation() {
        if (!Modernizr.geolocation) {
            alert("This browser doesn't support the Geolocation API.");
            return;
        }
        navigator.geolocation.getCurrentPosition(OnSuccess, OnError);
    }

    function OnError(err) {
        alert("Failed to retrieve your location. Try entering in your Zip code");
    }

    function OnSuccess(position) {
        MoveMapToPosition(map, position.coords.latitude, position.coords.longitude);

        //record the users lat long on the server and then load the search results
        $.getJSON(setLatLongURL, { Latitude: position.coords.latitude, Longitude: position.coords.longitude }, function (data) {
            //Highlight the users state
            $("option[value='" + data.UsersState + "']").selected(true);
        });
    }

A button Click calls GetLocation(). Inside that method, we check for "Modernizr.geolocation to make sure the user has a supported browser. Then we call navigator.geolocation.getCurrentPosition and pass it a Success and Error functions. That's it! There is no script to call. In the OnSuccess function you can see I get the coordinates from position.coords. You can then pass that to whatever mapping method you are using. Aside from moving the map to the position, you might notice I make a server side call. This call performs two functions, it stores the Lat long in the session for use on other pages. And it returns the State where the point is located so that we can pre-highlight that state for the user on the home page. I do this on the server through a call to the Bing Maps REST API

public ActionResult SetUsersLatLong(double Latitude, double Longitude)
        {
            Session["Latitude"] = Latitude;
            Session["Longitude"] = Longitude;

            //Figure out the state
            string key = "your_map_key";
            string query = Latitude + "," + Longitude;

            Uri geocodeRequest = new Uri(string.Format("http://dev.virtualearth.net/REST/v1/Locations/{0}?&key={1}", query, key));
string UserState;

            GetResponse(geocodeRequest, (x) =>
            {
                UsersState  = ((BingMapsRESTService.Common.JSON.Location)x.ResourceSets[0].Resources[0]).Address.AdminDistrict;
            });
            
            return Json(new {UsersState = UsersState }, JsonRequestBehavior.AllowGet);
}

private void GetResponse(Uri uri, Action<response> callback)
        {
                WebClient wc = new WebClient();
                var result = wc.OpenRead(uri);
                DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Response));
                callback(ser.ReadObject(result) as Response);
        }

</response>

That is the call to the REST service I make to get Geocode information for a Lat Long point. Now its not as simple as just this. You also need to include in your project somewhere the Bing Map REST Data Contracts. Copy and paste them from http://msdn.microsoft.com/en-us/library/jj870778.aspx into a CS file and include the namespace BingMapsRESTService.Common.JSON on your page that is making the calls.

In the Compare feature, we show companies side by side. If we have the users location, we can calculate the distance from the company to them. Here is the code for doing that if you have two sets of Lat Long points.

public double DistanceBetweenPoints(double latitude1, double longitude1, double latitude2, double longitude2)
        {
            double e = (3.1415926538 * latitude1 / 180);
            double f = (3.1415926538 * longitude1 / 180);
            double g = (3.1415926538 * latitude2 / 180);
            double h = (3.1415926538 * longitude2 / 180);
            double i = (Math.Cos(e) * Math.Cos(g) * Math.Cos(f) * Math.Cos(h) + Math.Cos(e) * Math.Sin(f) * Math.Cos(g) * Math.Sin(h) + Math.Sin(e) * Math.Sin(g));
            double j = (Math.Acos(i));
            double k = (6371 * j);

            return k;
        }

I wish I could claim that setting up the Authentication for the site was difficult. Surely it takes a lot of skill and effort to allow for authentication against several 3rd party services? But no, its actually very easy when you are starting with an ASP.NET MVC 4 template in Visual Studio. All of the hard work is handled for you using Oauth. Just open your App_Start/AuthConfig.cs and uncomment out the services you want to use. For this site, I went with Microsoft, Facebook, and Google. Google is the easiest to setup since it doesn't even require an app ID/Secret. But Microsoft and Facebook were easy enough to apply for dev accounts and app ID's

OAuthWebSecurity.RegisterMicrosoftClient(
                clientId: "-----",
                clientSecret: "-----");

            //OAuthWebSecurity.RegisterTwitterClient(
            //    consumerKey: "",
            //    consumerSecret: "");

            OAuthWebSecurity.RegisterFacebookClient(
                appId: "-----",
                appSecret: "---");

            OAuthWebSecurity.RegisterGoogleClient();

I set up the pages to seamlessly handle when the user is logged in or not. I don't want the user to have to log in to use the site. So in the View's, I use some Razor syntax to only insert the Favorites logic when the user is signed in. In my Places.cshtml page we have

<input name="cb_Checkin"  önchange="cbChecked(this)" value="@item.DunsId" />

                            
                            @if (User.Identity.IsAuthenticated)
                            {
                                
                                    @if (item.IsFavorite)
                                    {
                                        <img title="favorite"  önclick="toggleFavorite('@item.DunsId', this)" src="~/Images/Home/favorite.png" />
                                    }
                                    else
                                    {
                                        <img  önclick="toggleFavorite('@item.DunsId', this)" src="~/Images/Home/not_favorite.png" />
                                    }
                                
                                
                            }
                            
                                <a href="~/Home/Details?dunsId=@item.DunsId">@item.Name</a>

This project is my first foray into MVC and I have to admit, the Razor syntax takes some getting used to. But when you learn it well, its very powerful. However, I found myself getting carried away and making what I think is newbie mistakes. First off, I was passing too much stuff to the page in the form of ViewBag.Item = something. You should really stick to one overall Model for each View, whether its a single item or a list of items. If you need multiple types of items, then look into Partial Views, which I'll show in a bit. Another terrible path I went on was using Razor in my actual JavaScript. Only use Razor in the JS for stuff like var someMethodUrl = @Url.Action("Method"); If you find yourself needing to use Razor to inject data or items into the JS, then look into using Ajax calls to retrieve the data from the server after the browser has already loaded. This is cleaner code and it will result in faster page loads. Here is an example where I pull in Lat Long coordinates AND I utilize a partial view.

    var latLongURL = '@Url.Action("GetLatLong")';
    var detailsURL = '@Url.Action("CompanyDetails")';

function cbChecked(checkBox) {
        if (checkBox.checked == true) {
            $.getJSON(latLongURL, { dunsId: checkBox.value }, function (data) {
                if (data != null) {
                    //Get the location from the lat long
                    MoveMapToPosition(map, data.Latitude, data.Longitude);
                }
            });

            $("#detailsDiv").load(detailsURL + "/?dunsId=" + checkBox.value);
        }
    }

So the Checkbox contains a dunsId in its value property, and on checking it calls the method passing itself as a parameter. We pull in the Lat Long from the server, use MoveMapToPosition to move the map, and then we load in the Partial View for details. Here is what the server side looks like in our HomeController.cs class.

public ActionResult GetLatLong(string dunsId)
        {
            Company company = new Company() { DunsId = dunsId };
            company.PopulateLatLong();

            return Json(company, JsonRequestBehavior.AllowGet);
        }


        public ActionResult CompanyDetails(string dunsId)
        {
            Company company = new Company() { DunsId = dunsId };
            company.PopulateLatLong();
            company.PopulateDemographics();

            return PartialView("_CompanyDetails", company);
        }

The GetLatLong is straight forward. Takes in the dunsId, calls our Company models PopulateLatLong which queries the DnB service for the Lat Long. And then returns the JSONified Company object. For the CompanyDetauls Action method, we return a PartialView. Here is what that _CompanyDetails.cshtml file looks like. Partial views are a great way to break up a complicated page into manageable parts while keeping the Model View paradigm

@model DnBSite.Models.Company

<fieldset>
    <legend>@Model.Demographics.Company</legend>
    
@Model.Demographics.Address

     @Model.Demographics.City, @Model.Demographics.ZipCode @Model.Demographics.StateAbbrv

    
Phone: @Model.FormatedPhoneNumber

    
Fax: @Model.FormatedFaxNumber

</fieldset>

I've shows Bing Maps, Ajax, MVC, Jquery, Razor. But what about actual DnB? Here is the method I went with. It may not be considered best practice, but it worked for me for getting this site up quick. Basically, I noticed early on that most of the DnB tables were incredibly similar. They all include the basic DunsID, Name, State, Zip, Phone Number, etc... For the most part, each table is a copy of the Demographics table, except that it contains some extra details. So to start things off, here is my Places Action method (Places is the main page for showing results). I'm going to leave some sections of it out for simplicity sakes, such as the handling of null search/state parameters.

public ActionResult Places(string searchType, string states)
        {
//...
//Handle params....
//...

var data = new DnB.DNBDeveloperSandboxContainer(new Uri(SITE_URL));
            data.Credentials = new NetworkCredential(USER_ID, USER_ACCOUNT_KEY);

List<Company> Companies = new List<Company>();
            //Loop over states (i know this is dumb...)
            foreach (string state in states.Split(new char[] { ',' }))
            {
                if (searchType.Equals("minority"))
                {
                    foreach (var place in data.Minority.Where(p => p.StateAbbrv == state).ToList())
                    {
                        Companies.Add(new Company(){
                            DunsId = place.DUNSNumber,
                            Minority = place
                        });
                    }
                }
                else if (searchType.Equals("women"))
                {
                    foreach (var place in data.Women.Where(p => p.StateAbbrv == state).ToList())
                    {
                        Companies.Add(new Company()
                        {
                            DunsId = place.DUNSNumber,
                            Women = place
                        });
                    }
                }
                else if (searchType.Equals("veterans"))
                {
                    foreach (var place in data.Veteran.Where(p => p.StateAbbrv == state).ToList())
                    {
                        Companies.Add(new Company()
                        {
                            DunsId = place.DUNSNumber,
                            Veteran = place
                        });
                    }
                }
                else if (searchType.Equals("disadvantaged"))
                {
                    foreach (var place in data.Disadvantaged.Where(p => p.StateAbbrv == state).ToList())
                    {
                        Companies.Add(new Company()
                        {
                            DunsId = place.DUNSNumber,
                            Disadvantaged = place
                        });
                    }
                }
            }

//....
//code to handle Favorites if user is logged in
//....



return View(Companies);

}

That looks pretty ugly for what I was trying to do. But I'll explain why I had to use this method. First off, you may be wondering what the heck I need to do a loop over states for. Why not just call data.Disadvantaged.Where(p => statesString.Contains(p.StateAbbrv))? I tried this and I got a runtime error saying String.Contains is not supported. Thus I have to check each state explicitly. Another thing I tried to do was just grab the DunsID from each list using a Select(p => p.DunsId). But again I got a runtime error saying Select wasn't supported. Another thing you may notice is that I'm setting an attribute in my Company class to the actual database class items (Minority = item; etc...). In my Company class, I actually store attributes for Minorty, Women, Veteran, etc... Then when I make a call to Company.Name, it handles it like so

public class Company
{
    //....

    public string Name
        {
            get
            {
                if (Minority != null)
                    return Minority.Company;
                if (Women != null)
                    return Women.Company;
                if (Veteran != null)
                    return Veteran.Company;
                if (Disadvantaged != null)
                    return Disadvantaged.Company;
                if (Demographics != null)
                    return Demographics.Company;
                if (Firmographics != null)
                    return Firmographics.Company;

                return null;
            }
        }

    //....
}

I'm sure a lot of people will consider this a bad practice approach. But it was easy to set up and it was the best way I could figure out where I could generalize the Places() action method to use whatever List the user wanted. So in my View I can call @companyItem.Name without having to worry if I created the Company from a Minority item or a Veterans item.

How did I do that cool "Compare" pop up? Here is the quick and dirty JQuery for it.

<input id="btnCompare" type="button" title="Compare up to 3 Companies" class="btn btn-primary" value="Compare" onclick="compare()" />
            <div class="messagepop pop"></div>

<script>
//...

function compare() {
        var itemsToCompare = new Array();

        $('input:checked').each(function (index, data) {
            itemsToCompare.push(data.value);
        });

        var postData = { dunsIdList: itemsToCompare };
        $("body").append('<div class="modalOverlay">');
        $(".pop").load(compareURL, $.param(postData, true), function () {
            $(".pop").slideFadeToggle(function () {
                $(".close").click(function () {
                    deselect();
                    return false;
                });
            });
        });
    }

    $.fn.slideFadeToggle = function (easing, callback) {
        return this.animate({ opacity: 'toggle', height: 'toggle' }, "fast", easing, callback);
    };

//...
</script>

The server side for the compare returns a Partial View.

public ActionResult CompareCompanies(List<String> dunsIdList)
{
    List<Company> Companies = new List<Company>();

    if (dunsIdList != null)
    {
        foreach (var dunsId in dunsIdList)
        {
            Company company = new Company() { DunsId = dunsId };
            company.PopulateFirmographics();

            Companies.Add(company);
        }
    }

    return PartialView("_Compare", Companies);
}

The _Compare.cshtml View page.

@model List<DnBSite.Models.Company>

<table class="table table-bordered table-hover">
    <tr>

        @foreach (var item in Model)
        {
            <td>
                <table class="table table-hover" style="width: 210px">
                    <tr>
                        <td>
                            <div title="@item.DunsId" class="small_map_div" style="removed:relative; width: 200px; height: 200px;"></div>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            @item.Name
                        </td>
                    </tr>
                    <tr id="tr_distance_@item.DunsId" class="distance_tr">
                        <td id="td_distance_@item.DunsId">
                            test
                        </td>
                    </tr>
                    <tr>
                        <td>
                            Since @item.Firmographics.CompanyStartYear
                        </td>
                    </tr>
                    <tr>
                        <td>
                            @item.Firmographics.EmployeesTotal Total Employees
                        </td>
                    </tr>
                    <tr>
                        <td>
                            @item.Firmographics.LineOfBusiness
                        </td>
                    </tr>
                </table>

            </td>
        }
    </tr>

</table>
<input type="button" class="close btn btn-primary" value="Close" />

<script charset="UTF-8" type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script src="~/Scripts/BingMapFunctions.js"></script>
<script>
    $(".distance_tr").hide();

    $(document).ready(function () {
        $(".small_map_div").each(function (index, mapDiv) {
            $.getJSON(latLongURL, { dunsId: mapDiv.title }, function (data) {
                if (data != null) {
                    var map = GetMap(mapDiv);
                    MoveMapToPosition(map, data.Latitude, data.Longitude);

                    if (data.DistanceFromUser > 0) {
                        $("#td_distance_" + data.DunsId).text(data.DistanceFromUser + " Kilometers away");
                        $("#tr_distance_" + data.DunsId).show();
                    }
                }
            });
        });
    });
</script>

I have more code to talk about, but its 11:49 and I better submit before its too late. Good luck to everyone!

Points of Interest

I'm very greatful to DnB for this contest and the ability to use some of their data. But I was dissapointed in the data for several reasons. The amount of data set aside for us isn't enough to really let my app shine. I had wanted the user to be able to see several businesses for a single zip code, but there are only a couple thousand records for the entire US. Also the tables are very limited when it comes to searching. You can only query on indexed fields, which is basically just dunsID, State, and Zip. Otherwise, I found it very easy to get started fast with the DnB service and documentation.

I love the HTML5 Geocoding feature to get the users location. But for some reason it doesn't work in IE10 for me. It does work for Firefox however.

Please excuse any bugs or bad looking page elements. I came into this contest late. But please leave feedback because I actually really want to make my website work. I know a lot of my friends who are having trouble finding work and I think a site like this could be very useful for a lot of people.

History

9:00 pm, initial write up.
11:55 pm, put in code.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)