Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Fundraiser Focus - A D&B Project

4.86/5 (4 votes)
31 Jul 2013CPOL16 min read 19K   170  
Helping non-profit organizations target their fundraising efforts.

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 

Have you ever tried to ask a complete stranger for money for your cause? How did that go? If you asked 100 random people, what percentage do you think would donate? 1%? 5%? Now what if instead of random people, you asked people who had a know interest in your type of cause. Do you think that percentage would be higher? Sure it would. That is what Fundraiser Focus provides.

Overview

Fundraiser Focus is a web-based application for intelligently targeting businesses and business leaders for donations. Search parameters can be specified that identify organizations and leaders that are most likely to respond to a given request, thus improving ROI (return on investment) by increasing the odds of success. Those users who are preparing to meet with a given business leader can also pull up a full company report before the meeting to familiarize themselves with the company. 

Live Site 

You can find Fundraiser Focus at http://fundraiserfocus.azurewebsites.net. Feel free to play around all you want with the demo data. Your data will be stored in memory only which keeps it clean for the next user.   

Application Concept

The concept of Fundraiser Focus is that non-profit organizations have a limited amount of resources to use to get donations. The more money they spend trying to get donations, the less they have for their cause. Fundraiser Focus improves this situation by providing targeted lists of organizations to seek out for donations. For instance, if you are doing an Earth Day event, companies that are Green-certified would be more likely to be in tune with your cause. It might even be that there is more than one group that you might want to target. For instance, a campaign to fight poverty would be of interest to companies in labor surplus areas, Hub Zones, etc. As an organization, you might have different tactics based upon which group of organizations you are targeting.

Fundraiser Focus handles all of this with ease. The primary method of obtaining information is a campaign. This is your over-arching fundraising activity. Inside of a campaign, you can have one or more searches. Each search represents a target group. When you process a campaign, matches are generated for each search. Basic company contact information is generated for every company in the list. However, it may be that once a user has chosen to contact a specific company, they may feel that they need more information about the company. They can get this information by requesting a company research report. This report is a comprehensive set of data that flags important items of note including bankruptcies, liens, and annual income.

Application Walkthrough 

The Fundraiser Focus website is a full-featured site set up as if it were an actual company. When you hit the landing page, you will get an overview of what the software can do as if you were a potential customer:

All of the pages are set up and are useful for identifying more information about the service. However, the most concise information about what all the code can do can be found on the pricing page, where each service is briefly explained:

However, the purpose of this site (and for this entry in the D&B Contest) is found on the Demo Page. This page demonstrates what a customer of Fundraiser Focus will experience when they sign up for an account. Conveniently, this also allows me to show off the features of the application without needing people to create a login. If I were to turn this into an actual service, I would restrict the demo page more since this could rack up quite the bill but since this is using sandbox data, it is fine.

When you initially hit the demo page, you will see the demo help text. This page gives you some suggestions for how to look up the data. It is important to note here that this page is pre-populated with data from D&B. This data was downloaded and stored for later retrieval. We could access the data live every time but I decided not to his the server every time for the same test data. When you create a new campaign or a new company research report, the data will be pulled directly from D&B. This feature also then allows you to test creating new records without changing the demo for the next user. Your changes are stored in memory and are wiped out when you leave.

The first place to poke around is the Campaign information. Select one of the search results under the existing campaign and you should see something similar to this:

 

As you can see, each search has a brief description that explains what the search returns and how the search will be used. This information is specified by the person who generated the campaign. Next you will see what criteria were used to generate the search results. Finally, you will see a list of organizations that match the search criteria. If you click on a company, the entry will expand to show you the contact information for that organization. At the bottom of the contact information is a link to generate a research report for that particular company. When you click on it, you will be taken to the report:

A company research report tells the viewer what is important about the company. It gives the basic overview of the company, sharing contact information, information about the leader, and other relevant details. It also highlights information that might indicate an issue with the company such as outstanding liens or bankruptcies.

Getting back to the campaigns, you will want to try out setting up your own campaigns (that is the purpose of a demo after all). To do so, hit the plus button next to the "Campaigns" title to get the New Campaign setup screen: 

Here you can create one or more searches that will aid your campaign goals. You will notice that there is a plus button next to the "Company Research" title as well. If you want to look up a company by DUNS number instead of by clicking on the link in the search results, click the plus button and enter it. The resulting report will be entered in the list like any other result. 

Application Technologies

The Fundraiser Focus site is built using the following technologies primarily:

  • D&B data via OData
  • C#
  • ASP.NET MVC
  • JavaScript
  • Knockout
  • Windows Azure
  • Twitter Bootstrap 

The website is hosted on Windows Azure. The data also comes from Windows Azure. In order to gain the best performance out of the site, the controller that handles the demo page has a couple ActionResults that return JSON data instead of Views. The reason for this is so that the demo page can utilize AJAX calls instead of the more time-consuming full-page refreshes. This also gives the site the feel of a desktop application. Knockout rounds out the technologies, providing two-way data binding in order to wire up the data returned from the AJAX calls.

Key Code Sections

OK, enough marketing speak and sales talk. The real fun part is the code, right? Or is that just me? Fundraiser Focus is a fairly standard ASP.NET MVC web application but, like with any application, it has some tricky code bits. Let's go over these so we can all learn from what I figured out (and my mistakes).

ASP.NET MVC

Whenever you make an ASP.NET MVC (from here on out referred to just as "MVC" for short) application, you have a number of foundational issues to decide before you move forward. My first decision was how to deal with my routes. I wanted a nice URL structure and I wanted to keep my design flexible. My first thought was to put all actions in one controller. This allow me to keep things simple, since most of my actions just returned the appropriate view. The problem was that my URL structure would look like this:

  • /Home/Features
  • /Home/AboutUs
  • /Home/FAQ

That isn't a very intuitive URL structure. Of course, their is a fix for that. I created a new route (in the RouteConfig.cs file) and added it before the default route. The new route looked like so:

C#
routes.MapRoute(
   name: "CleanURLs",
   url: "{action}",
   defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
); 

What this route does it is looks at the URL and if it only has one item after the root of the website, it makes that item the action. It sets the controller to be the home controller. Now our routes look like this:

  • /Features
  • /AboutUs
  • /FAQ

That is more intuitive but it makes for a full controller. It also makes for tricky parts when I go to create my demo page, but that can be solved by creating a new controller just for the demo. Since I left the default route in place, I can still use other controllers. In the end, however, I decided that the future expansion of the site warranted a more robust design. As a result, I created a controller for each page and reverted my routes back to the standard MVC route set. This allows me to keep the nice URL scheme but it also allows me the possibility of future expansion like so:

  • /Features
  • /Features/Campaigns
  • /Features/BusinessReports
  • /AboutUs
  • /AboutUs/Tim
  • /FAQ
  • /FAQ/General
  • /FAQ/Billing

The next hurdle to overcome is how best to get the loaded data from the server-side to the client side. The easiest way, and the typical MVC way, would be to pass the model to the view directly via the ViewModel. While this is simple to do, it forces a full-page refresh. I decided a better solution would be to pass the data as JSON to the main view. That way I could consume it via AJAX calls and prevent the need for full-page refreshes. In order to do that, I created a couple of actions to return JSON instead of Views like so:

C#
public ActionResult GenerateCompanyReport(string dunsNumber)
{
   DnB data = new DnB();
   Models.DashboardModel model = new Models.DashboardModel();
   return Json(data.GetCompanyByDuns(dunsNumber), JsonRequestBehavior.AllowGet);
} 

Note that I'm allowing GET calls to access this action as well. We will get back to the client-side of this later in the Knockout section.

C#

To get the data from D&B, I connected via a service reference. This makes it easy to connect to the data and query for the specific entries we need. For example, to get the demographic data for a specific company by DUNS number, I use this one line:

var demographic = context.Demographics.Where(x => x.DUNSNumber == duns); 

Simple, right? Now the demographic variable has all of the demographic records I need (in this case, just the one). It gets a bit more complicated, however, when I want to query for specific sets of data. For instance, I have a series of check boxes that a user can check to indicate if they want certain criteria to be used as part of the filter. I have to only filter on an item if it is checked and I don't want to run a query against the same data twice. The final code I came up with looks like this: 

C#
var veteran = from orgs in context.Veteran
                where (orgs.VietnamVeteranOwnedIndicator == "Y" || 
                    orgs.VietnamVeteranOwnedIndicator == criteria.IsVietnamVeteranOwned.ToYOrBlank())
                where (orgs.ServiceDisabledVeteranOwnedIndicator == "Y" || 
                    orgs.ServiceDisabledVeteranOwnedIndicator == criteria.IsServiceDisabledVeteranOwned.ToYOrBlank())
                where (orgs.DisadvantagedVeteranEnterpriseIndicator == "Y" || 
                    orgs.DisadvantagedVeteranEnterpriseIndicator == criteria.IsDisadvantagedVeteranEnterprise.ToYOrBlank())
                select orgs; 

Note that I am using a custom helper method called ToYOrBlank. This method is a boolean overload that converts the resulting value to either a "Y" or a blank (which is the two possible values for the column).  Here is that code:

C#
public static string ToYOrBlank(this bool input)
{
    string output = string.Empty;
    if (input)
    {
        output = "Y";
    }
    else
    {
        output = null;
    }
    return output;
} 

Now if we look back at the linq statement, it should make a bit more sense. I am taking all of the items from the veterans list and I am filtering them by a series of paired values. Each pair is either a "Y" value or the conversion of the boolean variable. That way, if the item is checked, it will be "Y" or "Y" (only those items that are true) whereas if the box is not checked, it will be "Y" or blank (which would be every item). This allows me to easily filter the items based upon the checkboxes without more complicated logic at design time. Since that is when I am building my linq query, that makes the most sense. The final state is to then output this set of data to a list.

This brings us to our next issue: more than 100 records. If a query would return more than 100 records, only the first 100 will be returned (think about a query that would, in the real world instead of our sandbox, return 1,000,000 records - you wouldn't want that coming over the wire all at once). So, how do we get all of the records? Sounds like a job for recursion. Here is my final code (with the where statement removed to make it easier to read):

C#
private List<Veteran> VeteranLookup(SearchCriteriaModel criteria, int startPosition = 0)
{
    var veteran = from orgs in context.Veteran
                    select orgs;
    List<Veteran> output = new List<Veteran>();
    output = veteran.Skip(startPosition).ToList();
    if (output.Count == 100)
    {
        output.AddRange(VeteranLookup(criteria, startPosition + 100));
    }
    return output;
} 

See how if the count of the current data set is more than 100, the same method is called again and the records returned from that call are added to the output results. The result of this method is that you can return all of the matching rows, whether you have 10 rows coming back or 10,000. I use the skip operator and start position indicator to let me know where to go in the results. 

The final major hurdle I had to overcome was how to merge all of this data together in a way that didn't include duplicates in the results. Fortunately, Linq is a great tool that makes this type of this much simpler than it otherwise would be. The first thing I do is look at each set of data (veterans, minorities, etc.) and see if I need to filter data by that category. If I do, I filter the data and get the results back. I then look to see if the returned results are my first resultset to come back or if there are no records (since if we filter on data and return no results then we should have no records at all). This look like so:

C#
if (criteria.IsMinority)
{
    minorities = MinorityLookup(criteria);
    if (!searchedData || minorities.Count == 0)
    {
        output = (from model in minorities
                    select new BasicCompanyModel
                    {
                        DunsNumber = model.DUNSNumber,
                        CompanyName = model.Company,
                        Address = model.Address,
                        City = model.City,
                        StateAbbrv = model.StateAbbrv,
                        ZipCode = model.ZipCode,
                        Phone = model.Phone,
                        Fax = model.Fax
                    }).ToList();
    }
    searchedData = true;
} 

Once I do that for each set of data, I then start the merge. First, I check to be sure that the searchData variable is true (which means at least one filter was applied successfully) and that the output has records (since if it doesn't, we don't need to try to merge anything - we are returning an empty recordset anyway). Inside that statement, I check each group to see if there are records that should be merged in. If there are, I do a Linq query on the output and the current group like so:

C#
if (minorities.Count > 0)
{
    output = (from curr in output
                join adds in minorities on curr.DunsNumber equals adds.DUNSNumber
                select new BasicCompanyModel
                {
                    DunsNumber = curr.DunsNumber,
                    CompanyName = curr.CompanyName,
                    Address = curr.Address,
                    City = curr.City,
                    StateAbbrv = curr.StateAbbrv,
                    ZipCode = curr.ZipCode,
                    Phone = curr.Phone,
                    Fax = curr.Fax
                }).ToList();
} 

Notice that I'm doing an inner join on the results. The reason for this is because I need to find the companies that are in every filtered grouping. For instance, if I'm looking for green companies that are minority-owned, I don't want just green companies. They need to have both criteria to be chosen.  Also note that I am outputting the results to a list of BasicCompanyModel. I can then just pass the final output variable back to the calling method as is without need for further conversion. I would encourage you, if you still have questions about how I did this, to check out the DnB.cs file to look at the entire GetSearchResults method. 

Knockout

I decided to use Knockout to handle the client-side data binding. This allows for a more "desktop" application feel to web apps. The first hurdle Knockout helped me fix was how to get the data from D&B one time and then store it for later use (so we aren't downloading the same initial demo data every time). I solved this by saving the JSON results of the page into a text file. I then use AJAX to load the data from the text file once the page is ready and then I data bind that information like so: 

JavaScript
$.ajax({
    type: "GET",
    url: 'data/dashboard.txt',
    cache: true,
    dataType: "json",
    contentType: "json",
    success: function (response) {
        ko.applyBindings(new demoJS.vm(response));
    },
    error: function (response) {
        alert('An error has occured while processing your request');
    }
});  

The ko.applybindings line is where I hook up Knockout. I am using the mapping plug-in to make my life easier. It takes my data and maps it out by wrapping the data with observable or observablearray functions as necessary. 

The next issue that Knockout (and the mapping plug-in specifically) helped me overcome was when I returned a new campaign. Remember that my Demo controller is sending back JSON data when I send in a campaign request. The problem I experienced was that since it was such a nested set of arrays and items, it was difficult to get the mapping right to push it onto the existing array of campaigns. I solved this issue by using the mapper again like so:

JavaScript
$.ajax({
    type: "POST",
    url: '/Demo/ProcessNewCampaign',
    data: campData,
    cache: false,
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    success: function (response) {
        var newData = {};
        ko.mapping.fromJS(response, {}, newData);
        self.Campaigns.push(newData);
        self.currentSearch(newData.Searches()[0]);
        self.makeSearchVisible();
    },
    error: function (response) {
        toastr.error('There was an error processing your campaign. Please check the data and try again.', 'Process Campaign Error');
    }
});  

As you can see, I created a new array variable and then mapped the response data into that new variable. This put the appropriate observable and observablearray functions on my data so that when I then pushed that into my existing array, it matched perfectly. 

If you look at the above code, you will also note the last two lines in the success event. I am setting the current search item to be the first result from the passed-back data and I'm making the search item the one that is visible. The result that the end user sees is that the new campaign data is displayed on the screen automatically. This lends the air of that fluid, real-time application that we want. 

The next hurdle that Knockout and JavaScript in general helped me overcome was translating the returning truthy values to green checkmarks and conversely translating the falsey values to red x's. To do this, I defined the html to look like this (I'm using Twitter Bootstrap and font-awesome):

HTML
<i data-bind="attr: { class: yesNoIcon(currentCompany().IsMinority) }"></i> 

What this does is it sets the class to be the result of the yesNoIcon function with the passed-in value of IsMinority. So let's look at that function:

C#
self.yesNoIcon = function (indicator) {
    if (typeof indicator !== "undefined") {
        if (indicator === 'Y' || indicator === 'B') {
            return 'icon-ok green';
        } else {
            return 'icon-remove red';
        }
    } else {
        return 'icon-remove red';
    }
};  

Got it?  Basically I'm first checking to be sure that indicator (the passed in value) is not undefined. Next, I'm checking to see if the value is either Y or B. The reason for this is because the data is not consistent (for my purposes). For some data columns, a Y represents an affirmative but for the Bankruptcy column, a B is an affirmative indicator. Anything else gets the negative classes. I say classes because you may notice that there are actually two classes being returned. The first is the type of icon to show. The second is the color that icon should be. Knockout has no problem adding both of these classes at once (I was a bit surprised at this). 

Conclusion 

So that is Fundraiser Focus. I set out to develop an application that would aid non-profit organizations in their efforts to raise funding for their projects. I believe I have accomplished this task with the help of the data from the D&B sandbox. I've attached my code to this article (minus my CSS files, which I am not at liberty to distribute). Feel free to poke around on the site and in the code. I will be happy to answer any questions you might have.  

Change Log 

July 29, 2013 

  • Initial release 
 July 31, 2013 

  • Formatted Net Worth and Annual Income as Currency
  • Set focus on input box when researching a new company
  • Formatted warnings as red boxes and hid them if they do not apply
  • Moved back to the top of the page when displaying new results
  • Added a "Download Results" button to search results (limited IE support - works best in Chrome at the moment)
  • Allowed for clicking on labels next to checkboxes for Add Search criteria section
  • Hitting enter after typing in a DUNS number in the new company research report box now submits the form properly 

License

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