Tuesday, June 30, 2015

Bucket List Demo (JavaScript and .Net examples using RESTful API and JSON)

Here are some examples I put together that I really enjoy, because of their simplicity.

The HTML example demonstrates the following:

- MySafeInfo's WICK API (RESTful)
- JavaScript
- jQuery (2.1.4)
- Bootstrap (3.3.5)
- Font Awesome (4.3.0)
- AJAX (asynchronous and synchronous)

 <!DOCTYPE html>  
   
 <html xmlns="http://www.w3.org/1999/xhtml">  
   <head>  
     <title>Bucket List</title>  
   
     <style type="text/css">  
       body {  
         margin: 0px 25px !important;  
       }  
   
       td.icons {  
         font-size: 18px !important;  
       }  
   
       td.icons i {  
         cursor: pointer;  
         padding: 0 15px;  
       }  
   
       .icon-cancel.disabled {  
         color: #cccccc;  
         cursor: not-allowed;  
       }  
     </style>  
   
     <!-- jquery -->  
     <script src="//code.jquery.com/jquery-2.1.4.min.js"></script>  
   
     <!-- bootstrap -->  
     <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">  
     <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>  
       
     <!-- font awesome -->  
     <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" />  
   
     <!-- custom script -->  
     <script type="text/javascript">  
       // variables  
       var readerKey = "28SFkEKyZucdjEvaqYGLD3SY2d9dsnqb";  
       var insertKey = "3InDNvlJnYzSGAHRjT2tUEB4MRygJtuV";  
       var updateKey = "iP3wwSUtuSLFoi4HNzmZIOazxckoldaN";  
       var deleteKey = "LqNQuiJ5Eg8FSzWBJZj2FLO0n0PDrM7L";  
       var table = "BucketList"  
   
       // format  
       String.prototype.format = function () {  
         var args = arguments;  
         return this.replace(/{(\d+)}/g, function (match, number) {  
           return typeof args[number] != 'undefined'  
            ? args[number]  
            : match  
           ;  
         });  
       };  
   
       $(function () {  
         // initialize  
         BindData();  
   
         // bind data  
         function BindData() {  
           // variables  
           var grid = $("#grid");  
   
           // ajax variables  
           var type = "GET";  
           var url = "https://mysafeinfo.com/wick/{0}/tables/{1}".format(readerKey, table);  
           var contentType = "application/json; charset=utf-8";  
           var dataType = "json";  
           var data = "";  
           var async = true;  
   
           // get the data from database table  
           $.ajax({  
             type: type,  
             url: url,  
             contentType: contentType,  
             dataType: dataType,  
             data: data,  
             async: async  
           }).done(function (result) {  
             // variables  
             var result = $(result);  
             var rows = "";  
             var columns = "";  
   
             // heading  
             $("h3.table-results").text("{0} record{1}".format(result.length, (result.length > 1 | result.length == 0) ? "s" : ""));  
   
             // get the template (schema) for this table  
             var template = GetTableTemplate();  
   
             // append a blank record so they can do an inline add in the grid  
             result.splice(0, 0, template);  
   
             // for each row  
             result.each(function (i, item) {  
               // variables  
               var key = "ID";  
               var id = item[key];  
   
               // clear columns  
               columns = "";  
   
               // begin new row  
               rows = "{0}<tr data-id='{1}'>".format(rows, id);  
   
               // get column list  
               for (var column in item) {  
                 // variables  
                 var value = item[column];  
   
                 // get column headers  
                 columns = "{0}<th>{1}</th>".format(columns, column);  
   
                 // check to see if this is an existing record or the blank record for the inline add  
                 // if an existing record show the data as-is  
                 // if the blank record show NEW for the ID column and empty textboxes for the other fields  
                 if (id > 0) {  
                   rows = "{0}<td data-column='{2}' data-value='{1}'>{1}</td>".format(rows, value, column);  
                 }  
                 else {  
                   if (column == key) {  
                     rows = "{0}<td data-column='{2}' data-value='0'><i class='fa fa-plus-circle icon-new' alt='New' title='New'></i></td>".format(rows, value, column);  
                   }  
                   else {  
                     rows = "{0}<td data-column='{2}' data-value='{1}'><input type='text' value='' maxlength='100' data-column='{2}'/></td>".format(rows, value, column);  
                   }  
                 }  
               }  
   
               // check to see if this is an existing record or the blank record for the inline add  
               // if an existing record show the edit and delete icons  
               // if the blank record show the save and cancel icons  
               if (id > 0) {  
                 rows = "{0}<td class='icons text-right'><i class='fa fa-pencil icon-edit' alt='Edit' title='Edit'></i><i class='fa fa-trash-o icon-delete' alt='Delete' title='Delete'></i><i class='fa fa-save icon-save hidden' title='Save' alt='Save'></i><i class='fa fa-times icon-cancel hidden' title='Cancel' alt='Cancel'></i></td></tr>".format(rows);  
               }  
               else {  
                 rows = "{0}<td class='icons text-right'><i class='fa fa-plus icon-save' title='Save' alt='Save'></i><i class='fa fa-times icon-cancel disabled' title='Cancel' alt='Cancel'></i></td></tr>".format(rows);  
               }  
             });  
   
             // columns  
             columns = "{0}<th></th>".format(columns);  
   
             // table  
             var table = "<table id='table-data' class='table table-striped table-responsive table-hover results'><thead><tr>{0}</tr></thead><tbody>{1}</tbody></table>".format(columns, rows);  
   
             // add data to grid  
             grid.html(table);  
           }).always(function () {  
               
           });  
         }  
   
         // edit  
         $("body").delegate("table.results tbody tr td i.icon-edit", "click", function () {  
           // variables  
           var td = $(this).parent("td");  
           var tr = $(td).parent("tr");  
   
           // toggle icons  
           $(td).children("i.icon-edit").addClass("hidden");  
           $(td).children("i.icon-delete").addClass("hidden");  
           $(td).children("i.icon-save").removeClass("hidden");  
           $(td).children("i.icon-cancel").removeClass("hidden");  
   
           // add input fields to each cell  
           for (var i = 1; i < tr.children("td").length - 1; i++) {  
             // variables  
             var cell = tr.children("td")[i];  
   
             // add input to cell  
             $(cell).html("<input type='text' value='{1}' maxlength='100' data-column='{0}'/>".format($(cell).data("column"), $(cell).text().trim()));  
   
             // set focus to the first input  
             if (i == 1) { $(cell).children("input").focus(); }  
           };  
   
           // keydown event to capture enter key  
           $(window).keydown(function (event) {  
             if (event.which == 13) {  
               event.preventDefault();  
               td.children("i.icon-save").trigger("click");  
             }  
           });  
         });  
   
         // delete  
         $("body").delegate("table.results tbody tr td i.icon-delete", "click", function () {  
           if (confirm("Are you sure you want to delete this record?")) {  
             // variables  
             var tr = $(this).parent("td").parent("tr");  
             var id = $(tr).children("td").first().text();  
   
             // ajax variables  
             var type = "DELETE";  
             var url = "https://mysafeinfo.com/wick/{0}/tables/{1}/{2}".format(deleteKey, table, id);  
             var contentType = "application/json; charset=utf-8";  
             var dataType = "json";  
             var data = "";  
             var async = false;  
   
             // delete  
             $.ajax({  
               type: type,  
               url: url,  
               contentType: contentType,  
               dataType: dataType,  
               data: data,  
               async: async  
             }).done(function (result) {  
               // bind data  
               BindData();  
   
               // show message  
               alert(result.Message);  
             }).always(function () {  
                 
             });  
           }  
         });  
   
         // save  
         $("body").delegate("table.results tbody tr td i.icon-save", "click", function () {  
           // variables  
           var tr = $(this).parent("td").parent("tr")  
           var id = tr.data("id");  
           var request = "";  
           var isValid = false;  
   
           // create request  
           for (var i = 1; i < tr.children("td:not(.icons)").length; i++) {  
             // controls  
             var input = $(tr.children("td")[i]).children("input");  
   
             // variables  
             var column = $(input).data("column");  
             var value = $(input).val().trim();  
   
             // append to request  
             request = '{0} "{1}":"{2}",'.format(request, column, value)  
   
             // is valid flag if at least one field has data  
             if (value.length > 0) {  
               isValid = true;  
             }  
           }  
   
           // make sure at least one field has data, otherwise nothing to add/update  
           if (!isValid) {  
             alert("Please enter a value for at least one field.");  
             tr.children("td").find("input").first().focus();  
             tr.children("td").find("input").first().select();  
             return;  
           }  
   
           // format json request  
           request = "{{0}}".format(request.substring(1, request.length - 1));  
   
           // check to see if we need to do an insert or an update  
           if (id > 0) {  
             // ajax variables  
             var type = "PUT";  
             var url = "https://mysafeinfo.com/wick/{0}/tables/{1}/{2}".format(updateKey, table, id);  
             var contentType = "application/json; charset=utf-8";  
             var dataType = "json";  
             var data = request;  
             var async = false;  
   
             // update  
             $.ajax({  
               type: type,  
               url: url,  
               contentType: contentType,  
               dataType: dataType,  
               data: data,  
               async: async  
             }).done(function (result) {  
               // bind  
               BindData();  
   
               // message  
               alert(result.Message);  
             }).always(function () {  
               // remove key event listener  
               RemoveListener();  
             });  
           }  
           else {  
             // ajax variables  
             var type = "POST";  
             var url = "https://mysafeinfo.com/wick/{0}/tables/{1}".format(insertKey, table);  
             var contentType = "application/json; charset=utf-8";  
             var dataType = "json";  
             var data = request;  
             var async = false;  
   
             // insert  
             $.ajax({  
               type: type,  
               url: url,  
               contentType: contentType,  
               dataType: dataType,  
               data: data,  
               async: async  
             }).done(function (result) {  
               // bind  
               BindData();  
   
               // message  
               alert(result.Message);  
             }).always(function () {  
               // remove key event listener  
               RemoveListener();  
             });  
           }  
         });  
   
         // cancel  
         $("body").delegate("table.results tbody tr td i.icon-cancel", "click", function () {  
           // variables  
           var tr = $(this).parent("td").parent("tr")  
           var id = tr.data("id");  
   
           // short-circuit if this is a new record  
           if (id < 1) {  
             return;  
           }  
   
           // set defaults  
           for (var i = 1; i < tr.children("td:not(.icons)").length; i++) {  
             var td = $(tr.children("td")[i]);  
             td.text(td.data("value"));  
           }  
   
           // toggle icons  
           $(this).parent("td").children("i.icon-edit").removeClass("hidden");  
           $(this).parent("td").children("i.icon-delete").removeClass("hidden");  
           $(this).parent("td").children("i.icon-save").addClass("hidden");  
           $(this).parent("td").children("i.icon-cancel").addClass("hidden");  
   
           // remove key event listener  
           RemoveListener();  
         });  
   
         function GetTableTemplate() {  
           // variables  
           var template;  
   
           // ajax variables  
           var type = "GET";  
           var url = "https://mysafeinfo.com/wick/{0}/tables/{1}/template".format(readerKey, table);  
           var contentType = "application/json; charset=utf-8";  
           var dataType = "json";  
           var data = "";  
           var async = false;  
   
           // get table template  
           $.ajax({  
             type: type,  
             url: url,  
             contentType: contentType,  
             dataType: dataType,  
             data: data,  
             async: async  
           }).done(function (result) {  
             template = result;  
           }).always(function () {  
   
           });  
   
           // return  
           return template;  
         }  
   
         function RemoveListener() {  
           $(window).unbind("keydown");  
         }  
       });  
     </script>  
   </head>  
   
   <body>  
     <!-- container for header to indicate how many records were retrieved -->  
     <h3 class="table-results">0 records</h3>  
   
     <!-- container for html table built dynamically from our ajax call -->  
     <div class="table-responsive">  
       <div id="grid"></div>  
     </div>  
   </body>  
 </html>  

I've also included examples using .Net 4.5 (C# and VB.Net) for those looking to call a RESTful API from the server-side, which can be helpful when working with key-based APIs, so you can hide your API keys. The .Net 4.5 examples demonstrate the following:

- MySafeInfo's WICK API (RESTful)
- Json.NET (Newtonsoft.Json)
- WebClient.UploadString

 using System;  
 using System.Net;  
 using Newtonsoft.Json;  
                            
 public class Program  
 {  
      public static void Main()  
      {  
           // variables  
           string Table = "BucketList";  
           string InsertKey = "3InDNvlJnYzSGAHRjT2tUEB4MRygJtuV";  
           string DeleteKey = "LqNQuiJ5Eg8FSzWBJZj2FLO0n0PDrM7L";  
           string InsertUrl = string.Format("https://mysafeinfo.com/wick/{0}/tables/{1}", InsertKey, Table);  
           PostResult PostResult = new PostResult();  
           WickResult WickResult = new WickResult();  
           BucketList BucketList = new BucketList { Description = "Run a mini-marathon", Notes = "2016 OneAmerica 500 Festival Mini-Marathon", Date = "May 2016" };  
   
           // use Json.NET (Newtonsoft.Json) to serialize the data object  
           // https://www.nuget.org/packages/Newtonsoft.Json  
           string Data = JsonConvert.SerializeObject(BucketList);  
   
           // post the data to the MySafeInfo WICK API  
           PostResult = SubmitRequest(Url: InsertUrl, Data: Data);  
   
           // use Json.NET (Newtonsoft.Json) to deserialize json result into a strongly typed data object  
           // https://www.nuget.org/packages/Newtonsoft.Json  
           if (PostResult.Success)  
           {  
                WickResult = JsonConvert.DeserializeObject<WickResult>(PostResult.Message);  
           }  
   
           // debug  
           Console.WriteLine(string.Format("WICK Input: {0}", Data));  
           Console.WriteLine(string.Format("WICK Output: {0}", PostResult.Message));  
           Console.WriteLine(string.Format("ID: {0}", WickResult.ID));  
           Console.WriteLine(string.Format("Success: {0}", WickResult.Success));  
           Console.WriteLine(string.Format("Message: {0}", WickResult.Message));  
   
           // just for fun we are going to delete the record to show how simple a delete is  
           if (WickResult.Success)  
           {  
                Console.WriteLine(string.Format("Delete: {0}", SubmitRequest(Url: string.Format("https://mysafeinfo.com/wick/{0}/tables/{1}/{2}", DeleteKey, Table, WickResult.ID), Method: "DELETE").Message));  
           }  
      }  
   
      public static PostResult SubmitRequest(string Url, string Data = "", string Method = "POST", string ContentType = "application/json; charset=utf-8")  
      {  
           // variables  
           PostResult Result = new PostResult();  
   
           try  
           {  
                // web client  
                using (WebClient Client = new WebClient())  
                {  
                     // content type  
                     Client.Headers[HttpRequestHeader.ContentType] = ContentType;  
   
                     // post data and get result  
                     Result.Message = Client.UploadString(address: Url, method: Method, data: Data);  
                     Result.Success = true;  
                }  
           }  
           catch (Exception ex)  
           {  
                Result.Message = ex.Message;  
           }  
   
           // return  
           return Result;  
      }  
   
      // helper classes  
      public class BucketList  
      {  
           public string Description { get; set; }  
           public string Notes { get; set; }  
           public string Date { get; set; }  
      }  
   
      public class PostResult  
      {  
           public bool Success { get; set; }  
           public string Message { get; set; }  
      }  
   
      public class WickResult  
      {  
           public int ID { get; set; }  
           public bool Success { get; set; }  
           public string Message { get; set; }  
      }  
 }  

As you can see, the server side code is relatively simple since we can leverage Json.NET to serialize and deserialize our data, and we can use built in .Net namespaces (WebClient) to perform our RESTful operations against the API.

Here are links to each example:

Bucket List Demo (HTML, JavaScript, jQuery)
Bucket List Demo (.Net 4.5, C#)
Bucket List Demo (.Net 4.5, VB.Net)

Whether you are using client-side scripting or server side scripting, you can see that using JSON data with a RESTful API can be very easy to implement.

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey

Monday, June 29, 2015

ASP.Net C# Displaying Farmers Markets Data from MySafeInfo in Google Maps

This example uses a combination of technologies such ASP.Net, C#, JavaScript, jQuery, JSON, XML, and XSL to retrieve State Farmers Markets data from MySafeInfo and display the results in a Bootstrap table and on a Google Map.

Here's the ASPX page:

 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="Web.farmers_markets._default" %>  
   
 <!DOCTYPE html>  
   
 <html xmlns="http://www.w3.org/1999/xhtml">  
 <head runat="server">  
   <!-- title -->  
   <title>State Farmers Markets</title>  
   
   <!-- jquery -->  
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>  
   
   <!-- bootstrap -->  
   <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />  
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>  
   
   <!-- google maps -->  
   <script language="javascript" type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key={key}&sensor=false&language=en"></script>  
   
   <!-- scripts -->  
   <script language="javascript" type="text/javascript" src="../js/functions.js?cache=2015062801"></script>  
   <script language="javascript" type="text/javascript" src="../js/spin.min.js?cache=2015062801"></script>  
   
   <!-- custom style -->  
   <style>  
     body {  
       margin: 25px !important;  
     }  
   
     .form-group {  
       width: 100%;  
     }  
   
     #map-canvas {  
       width: 100%;  
       height: 500px;  
     }  
   </style>  
   
   <script language="javascript" type="text/javascript">  
     // initialize  
     $(document).ready(function () {  
       InitializeMap();  
     });  
   
     function InitializeMap() {  
       // variables  
       var options = { center: new google.maps.LatLng(0, 0), mapTypeId: google.maps.MapTypeId.ROADMAP, scrollwheel: false, draggable: true };  
       var map = new google.maps.Map(document.getElementById("map-canvas"), options);  
       var bounds = new google.maps.LatLngBounds();  
       var locations = jQuery.parseJSON($("#lblLocations").val());  
   
       // add locations to map  
       $.each(locations, function () {  
         // variables  
         var latLong = new google.maps.LatLng(this.latitude, this.longitude);  
         var Url = this.url.replace(/&amp;/g, "&");  
   
         // marker  
         var marker = new google.maps.Marker({  
           map: map,  
           title: "{0} ({1})".format(this.market, this.city),  
           position: latLong,  
           draggable: false,  
           icon: "images/marker.png"  
         });  
   
         // bounds  
         bounds.extend(latLong);  
   
         // click event  
         if (Url.HasValue()) {  
           google.maps.event.addListener(marker, 'click', function () { window.open(Url) });  
         }  
       });  
         
       // fit bounds  
       map.fitBounds(bounds);  
     }  
   
     function btnSearch_Click() {  
       // controls  
       var txtZipCode = $("#txtZipCode");  
   
       // variables  
       var ZipCode = txtZipCode.val().trim();  
   
       // validation  
       if (!IsValidZip(ZipCode)) {  
         ShowMessage('Please enter a valid zip code.', 'txtZipCode', 'txtZipCode');  
         return false;  
       }  
   
       // show progress  
       ShowProgress();  
   
       // return value  
       return true;  
     }  
   </script>  
 </head>  
   
 <body>  
   <form id="form1" runat="server">  
     <div class="form-group">  
       <h2>  
         <asp:Literal ID="lblTitle" runat="server" />  
       </h2>  
     </div>  
   
     <!-- map -->  
     <div class="form-group">  
        <div id="map-canvas"></div>  
     </div>  
   
     <!-- filter -->  
     <div class="form-group" style="padding-top: 15px">  
       <asp:Panel ID="pnlSearch" DefaultButton="btnSearch" runat="server">  
         <asp:DropDownList ID="ddlList" CssClass="form-control" Width="250px" Style="display: inline" ClientIDMode="Static" runat="server" />  
         &nbsp;within&nbsp;  
         <asp:DropDownList ID="ddlRadius" CssClass="form-control" Width="100px" Style="display: inline" ClientIDMode="Static" runat="server">  
           <asp:ListItem>25</asp:ListItem>  
           <asp:ListItem>50</asp:ListItem>  
           <asp:ListItem>75</asp:ListItem>  
           <asp:ListItem>100</asp:ListItem>  
         </asp:DropDownList>  
         &nbsp;miles of:&nbsp;  
         <asp:TextBox ID="txtZipCode" Text="46237" MaxLength="5" CssClass="form-control" Width="100px" Style="display: inline; margin-right: 10px" ClientIDMode="Static" runat="server" />  
         <asp:Button ID="btnSearch" Text="Search" CssClass="btn btn-primary" OnClientClick="javascript: return btnSearch_Click();" OnClick="btnSearch_Click" ClientIDMode="Static" runat="server" />  
       </asp:Panel>  
     </div>  
   
     <!-- source -->  
     <div class="form-group">  
       <em>This demo is powered by data from mysafeinfo.com</em><br />  
       <asp:HyperLink ID="lnkList" Target="_blank" ClientIDMode="Static" runat="server" />  
     </div>  
   
     <!-- grid -->  
     <div class="form-group">  
       <asp:Literal ID="lblResults" runat="server" />  
     </div>  
       
     <!-- hidden fields -->  
     <asp:HiddenField ID="lblLocations" ClientIDMode="Static" runat="server" />  
   
     <!-- progess -->  
     <div id="loading">  
       <div id="loadingcontent">  
         <p id="loadingspinner">  
         </p>  
       </div>  
     </div>  
   
     <!-- message modal -->  
     <div class="modal fade" id="modal-message" tabindex="-1" role="dialog" aria-labelledby="lblModalMessage" aria-hidden="true">  
       <div class="modal-dialog">  
         <div class="modal-content">  
           <div class="modal-header">  
             <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>  
             <h4 class="modal-title" id="lblModalMessage">Message</h4>  
           </div>  
           <div class="modal-body">  
             <div class="message"></div>  
           </div>  
           <div class="modal-footer">  
             <div class="pull-left">  
               <button id="btnMessageClose" type="button" class="btn btn-primary" data-dismiss="modal">Close</button>  
             </div>  
           </div>  
         </div>  
       </div>  
     </div>  
   
     <!-- hidden fields -->  
     <input type="hidden" name="lblFocus" id="lblFocus" />  
     <input type="hidden" name="lblSelect" id="lblSelect" />  
   </form>  
 </body>  
 </html>  

The only references not available on a content delivery network (CDN) are the following:

 <!-- scripts -->  
 <script language="javascript" type="text/javascript" src="../js/functions.js?cache=2015062801"></script>  
 <script language="javascript" type="text/javascript" src="../js/spin.min.js?cache=2015062801"></script>

These files can be downloaded directly using the following:

http://pavey.azurewebsites.net/js/functions.js
http://pavey.azurewebsites.net/js/spin.min.js

Next we'll look at the code-behind file:
 using System;  
 using System.Collections.Generic;  
 using System.IO;  
 using System.Linq;  
 using System.Net;  
 using System.Web;  
 using System.Web.UI;  
 using System.Web.UI.WebControls;  
 using System.Xml;  
 using System.Xml.Linq;  
 using Newtonsoft.Json;  
 using Utilities;  
   
 namespace Web.farmers_markets  
 {  
   public partial class _default : System.Web.UI.Page  
   {  
     protected void Page_Load(object sender, EventArgs e)  
     {  
       // bind lists and data  
       if (!Page.IsPostBack)  
       {  
         BindLists();  
         BindData();  
       }  
     }  
   
     private void BindData()  
     {  
       // variables  
       string List = ddlList.SelectedValue;  
       string Title = ddlList.SelectedItem.Text;  
       string Url = string.Format("https://mysafeinfo.com/api/data?list={0}&format=xml&select=cd,ct,nm,add,lat,lng,url&rootname=data&elementname=r&alias=cd=state,ct=city,nm=market,add=address,lat=latitude,lng=longitude,url=url", List);  
       XmlDocument XmlDoc = new XmlDocument();  
       string XmlData = string.Empty;  
       string XslData = string.Empty;  
       string XslFile = Path.Combine(Request.PhysicalApplicationPath, "farmers-markets", "xsl", "results.xsl");  
       string ZipCode = txtZipCode.Text.Trim();  
       int Radius = Convert.ToInt32(ddlRadius.SelectedValue);  
       List<Market> Markets = new List<Market>();  
   
       // labels  
       lblTitle.Text = Title;  
       lnkList.NavigateUrl = Url;  
       lnkList.Text = Url;  
   
       // geocode zip code  
       Coordinate Origin = GetLatLong(Address: ZipCode);  
   
       // request  
       using (WebClient client = new WebClient())  
       {  
         XmlData = client.DownloadString(Url);  
       }  
   
       // load xml into xml document  
       XmlDoc.LoadXml(XmlData);  
   
       // iterate through data first so we can do distance calculation  
       foreach(XmlElement x in XmlDoc.SelectNodes("//r"))  
       {  
         // variables  
         string Latitude = x.GetAttribute("latitude");  
         string Longitude = x.GetAttribute("longitude");  
         double OriginLatitude = Origin.Latitude;  
         double OriginLongitude = Origin.Longitude;  
         double DestinationLatitude = 0;  
         double DestinationLongitude = 0;  
   
         // parse  
         double.TryParse(Latitude, out DestinationLatitude);  
         double.TryParse(Longitude, out DestinationLongitude);  
   
         // calculate distance from origin  
         double Distance = GeoCodeCalc.CalculateDistance(OriginLatitude: OriginLatitude, OriginLongitude: OriginLongitude, DestinationLatitude: DestinationLatitude, DestinationLongitude: DestinationLongitude);  
         bool InRange = Distance <= Radius;  
   
         // add attribute indicating distance to origin  
         x.SetAttribute("distance", Distance.ToString());  
   
         // add attribute indicating if location is in range  
         x.SetAttribute("inrange", InRange.ToString().ToLower());  
   
         // add to markets list for easy conversion to json later  
         if (InRange)  
         {  
           Markets.Add(new Market() { state = x.GetAttribute("state"), city = x.GetAttribute("city"), market = x.GetAttribute("market"), address = x.GetAttribute("address"), latitude = x.GetAttribute("latitude"), longitude = x.GetAttribute("longitude"), url = x.GetAttribute("url") });  
         }  
       }  
   
       // xsl  
       using (Utilities.Xsl Xsl = new Utilities.Xsl())  
       {  
         using (Utilities.Xsl.XslConfiguration XslConfiguration = new Utilities.Xsl.XslConfiguration(XslFile))  
         {  
           // parameters  
           XslConfiguration.AddXslParameter("radius", Radius.ToString());  
           XslConfiguration.AddXslParameter("zipcode", ZipCode);  
           XslConfiguration.AddXslParameter("state", Title);  
   
           // transform  
           XslData = Xsl.TransformXml(XmlType: Utilities.Xsl.XmlTypes.XmlData, XmlSource: XmlDoc.OuterXml, XslConfig: XslConfiguration);  
   
           // check for errors  
           if (Xsl.ErrorDescription.HasValue())  
           {  
             XmlData = Xsl.ErrorDescription;  
           }  
         }  
       }  
   
       // bind grid  
       lblResults.Text = XslData;  
   
       // use Json.NET (Newtonsoft.Json) to serialize locations for the map
       // https://www.nuget.org/packages/Newtonsoft.Json
       lblLocations.Value = JsonConvert.SerializeObject(Markets);  
     }  
   
     private void BindLists()  
     {  
       // variables  
       string Result = string.Empty;  
       string Url = "https://mysafeinfo.com/api/data?list=all&format=json&sort=nm&select=lst,nm&lst=farmermarkets,startswith&alias=lst=list,nm=name";  
       List<MarketList> Lists = new List<MarketList>();  
   
       // request  
       using (WebClient client = new WebClient())  
       {  
         Result = client.DownloadString(Url);  
       }  
   
       // use Json.NET (Newtonsoft.Json) to deserialize json string into a strongly typed list
       // https://www.nuget.org/packages/Newtonsoft.Json
       Lists = JsonConvert.DeserializeObject<List<MarketList>>(Result);  
   
       // bind  
       ddlList.DataValueField = "List";  
       ddlList.DataTextField = "Name";  
       ddlList.DataSource = Lists;  
       ddlList.DataBind();  
   
       // default  
       ddlList.SetValue("farmermarketsin");  
     }  
   
     protected void btnSearch_Click(object sender, EventArgs e)  
     {  
       BindData();  
     }  
   
     public static Coordinate GetLatLong(string Address, string Country = "")  
     {  
       // variables  
       string Url = string.Empty;  
   
       // determine url  
       if (Address.HasValue() && Country.HasValue())  
       {  
         Url = string.Format("https://maps.googleapis.com/maps/api/geocode/xml?address={0}&components=country:{1}&sensor=false", Address.Trim(), Country.Trim());  
       }  
       else  
       {  
         Url = string.Format("https://maps.googleapis.com/maps/api/geocode/xml?address={0}&sensor=false", Address.Trim());  
       }  
   
       // return  
       return GetLatLongByUrl(Url);  
     }  
   
     public static Coordinate GetLatLongByUrl(string Url)  
     {  
          // variables  
          Coordinate Coordinate = new Coordinate();  
          XElement XmlElement = XElement.Load(Url);  
          XElement Status = (from x in XmlElement.Descendants() where x.Name.ToString().IsEqual("status") select x).FirstOrDefault();  
   
          // check status  
          if (Status != null && Status.Value.IsEqual("ok"))  
          {  
            // variables  
            string strLatitude = XmlElement.Element("result").Element("geometry").Element("location").Element("lat").Value;  
            string strLongitude = XmlElement.Element("result").Element("geometry").Element("location").Element("lng").Value;  
            double Latitude = 0;  
            double Longitude = 0;  
   
            // parse  
            double.TryParse(strLatitude, out Latitude);  
            double.TryParse(strLongitude, out Longitude);  
   
            // coordinates  
            Coordinate.Latitude = Latitude;  
            Coordinate.Longitude = Longitude;  
          }  
   
          //return value  
          return Coordinate;  
     }  
   
     private class MarketList  
     {  
       public string List { get; set; }  
       public string Name { get; set; }  
     }  
   
     private class Market  
     {  
       public string state { get; set; }  
       public string city { get; set; }  
       public string market { get; set; }  
       public string address { get; set; }  
       public string latitude { get; set; }  
       public string longitude { get; set; }  
       public string url { get; set; }  
     }  
   
     public class Coordinate  
     {  
       public double Latitude { get; set; }  
       public double Longitude { get; set; }  
     }  
   
     // Calculate Distance Between Geocodes in C# and JavaScript  
     // http://pietschsoft.com/post/2008/02/Calculate-Distance-Between-Geocodes-in-C-and-JavaScript  
     public static class GeoCodeCalc  
     {  
       public const double EarthRadiusInMiles = 3956.0;  
       public const double EarthRadiusInKilometers = 6367.0;  
   
       public static double ToRadian(double val) { return val * (Math.PI / 180); }  
       public static double DiffRadian(double val1, double val2) { return ToRadian(val2) - ToRadian(val1); }  
   
       public static double CalculateDistance(double OriginLatitude, double OriginLongitude, double DestinationLatitude, double DestinationLongitude)  
       {  
         return CalculateDistance(OriginLatitude, OriginLongitude, DestinationLatitude, DestinationLongitude, GeoCodeCalcMeasurement.Miles);  
       }  
   
       public static double CalculateDistance(double OriginLatitude, double OriginLongitude, double DestinationLatitude, double DestinationLongitude, GeoCodeCalcMeasurement Measurement)  
       {  
         // variables  
         double Radius = GeoCodeCalc.EarthRadiusInMiles;  
         if (Measurement == GeoCodeCalcMeasurement.Kilometers) { Radius = GeoCodeCalc.EarthRadiusInKilometers; }  
   
         // return  
         return Radius * 2 * Math.Asin(Math.Min(1, Math.Sqrt((Math.Pow(Math.Sin((DiffRadian(OriginLatitude, DestinationLatitude)) / 2.0), 2.0) + Math.Cos(ToRadian(OriginLatitude)) * Math.Cos(ToRadian(DestinationLatitude)) * Math.Pow(Math.Sin((DiffRadian(OriginLongitude, DestinationLongitude)) / 2.0), 2.0)))));  
       }  
     }  
   
     public enum GeoCodeCalcMeasurement : int  
     {  
       Miles = 0,  
       Kilometers = 1  
     }  
   }  
 }  

This example also relies on the following:

- Extensions.cs
- Xsl.cs
- results.xsl
- marker.png

This might look like a lot of code at first glance. But pulling all of this together in an ASP.Net Web Forms Application is very simple, and will help you understand the following key concepts:

- Using WebClient to retrieve XML data from a 3rd party RESTful web service
- Working with XML data using an XmlDocument and XmlElement
- Using Google Maps API to geocode a zip code
- Calculating the distance between two points
- Using an XSL transformation to show the results in a Bootstrap table
- Using Json.NET (Newtonsoft.Json) to serialize locations for the Google Map
- Using Json.NET (Newtonsoft.Json) to deserialize JSON data into a strongly typed list
- Using JavaScript and jQuery to iterate a JSON array
- Using JavaScript and the Google Maps API to add locations to the Google Map
- Using JavaScript and jQuery to validate a valid zip code is entered
- Using JavaScript, jQuery, and Bootstrap to show validation errors in modal
- Using JavaScript and jQuery to show custom progress spinner

Click here to see a full working example of this demo.

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey

Saturday, June 27, 2015

ASP.Net C# Finding Controls in Nested Repeaters on Button Click Event

Here's a simple way to find controls in a nested repeater on a button click event.

Let's start with the ASPX page.
 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="Web.nested_repeaters._default" %>  
   
 <!DOCTYPE html>  
   
 <html xmlns="http://www.w3.org/1999/xhtml">  
 <head runat="server">  
   <!-- title -->  
   <title>Repeater Test</title>  
   
   <!-- jquery -->  
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>

   <!-- bootstrap -->  
   <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />  
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>  
   
   <!-- custom style -->  
   <style>  
     body {  
       margin: 25px !important;  
     }  
   
     .form-group {  
       width: 800px;  
     }  
   </style>  
 </head>  
   
 <body>  
   <form id="form1" runat="server">  
     <asp:Repeater ID="lstCategories" runat="server">  
       <ItemTemplate>  
         <div class="col-sm-12">  
           <h2><%#Eval("Name")%></h2>   
               
           <asp:Repeater ID="lstTasks" DataSource='<%#Eval("Tasks")%>' runat="server">  
             <ItemTemplate>   
               <div class="form-group">   
                 <div class="col-sm-12">  
                   <div class="col-sm-2">  
                     <%#Eval("Name")%>  
                   </div>  
                   <div class="col-sm-10">  
                     <!-- hidden fields -->  
                     <asp:HiddenField ID="lblID" Value='<%#DataBinder.Eval(Container.DataItem, "ID")%>' runat="server" />  
                     <asp:HiddenField ID="lblValue" Value='<%#DataBinder.Eval(Container.DataItem, "Value")%>' runat="server" />  
   
                     <!-- textbox to show current value and to allow updates -->  
                     <asp:TextBox ID="txtValue" Text='<%#DataBinder.Eval(Container.DataItem, "Value")%>' MaxLength="100" CssClass="form-control" runat="server" />  
                   </div>  
                 </div>  
               </div>  
             </ItemTemplate>  
           </asp:Repeater>  
         </div>  
       </ItemTemplate>  
     </asp:Repeater>  
   
     <div class="col-sm-12" style="padding-top: 25px">  
       <asp:Button ID="btnSave" Text="Save Changes" CssClass="btn btn-primary" Style="margin-right: 25px" OnClick="btnSave_Click" ClientIDMode="Static" runat="server" />  
       <asp:Button ID="btnCancel" Text="Cancel" CssClass="btn btn-primary" OnClick="btnCancel_Click" ClientIDMode="Static" runat="server" />  
     </div>  
   
     <!-- debug -->  
     <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="padding-top: 25px">  
       <asp:PlaceHolder ID="pnlDebug" Visible="true" runat="server" />  
     </div>  
   </form>  
 </body>  
 </html>  

For this example I'm using lstCategories for the outer repeater and lstTasks for the nested repeater. The nested repeater (lstTasks) defines 3 controls:

lblID This is the hidden field for the ID to identify your record (e.g. primary key)
lblValue This is the hidden field representing the original value of the field
txtValue This is a textbox that defaults to the current value, but can be edited

Then there are 2 buttons.

btnSave Iterates the repeaters to access the controls
btnCancel Reloads the page

Then the code-behind looks like this:
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Web;  
 using System.Web.UI;  
 using System.Web.UI.WebControls;  
   
 namespace Web.nested_repeaters  
 {  
   public partial class _default : System.Web.UI.Page  
   {  
     protected void Page_Load(object sender, EventArgs e)  
     {  
       if (!Page.IsPostBack)  
       {  
         BindData();  
       }  
     }  
   
     private void BindData()  
     {  
       lstCategories.DataSource = GetData();  
       lstCategories.DataBind();  
     }  
   
     private List<Category> GetData()  
     {  
       // variables  
       List<Category> Categories = new List<Category>();  
   
       // category 1  
       Category Category1 = new Category() { Name = "General", Tasks = new List<Task>() };  
       Category1.Tasks.Add(new Task() { ID = 1, Name = "Name", Value = "John Wick" });  
       Category1.Tasks.Add(new Task() { ID = 2, Name = "Email", Value = "info@pavey.net" });  
       Category1.Tasks.Add(new Task() { ID = 3, Name = "Phone", Value = "" });  
       Category1.Tasks.Add(new Task() { ID = 4, Name = "Website", Value = "http://www.pavey.me/" });  
       Category1.Tasks.Add(new Task() { ID = 5, Name = "Comments", Value = "" });  
   
       // category 2  
       Category Category2 = new Category() { Name = "Social Media", Tasks = new List<Task>() };  
       Category2.Tasks.Add(new Task() { ID = 6, Name = "Twitter", Value = "https://twitter.com/matthewpavey" });  
       Category2.Tasks.Add(new Task() { ID = 7, Name = "Facebook", Value = "" });  
       Category2.Tasks.Add(new Task() { ID = 8, Name = "LinkedIn ", Value = "" });  
   
       // category 3  
       Category Category3 = new Category() { Name = "Favorite Websites", Tasks = new List<Task>() };  
       Category3.Tasks.Add(new Task() { ID = 9, Name = "Website", Value = "http://www.pavey.me/" });  
       Category3.Tasks.Add(new Task() { ID = 10, Name = "Website", Value = "https://mysafeinfo.com/" });  
       Category3.Tasks.Add(new Task() { ID = 11, Name = "Website", Value = "" });  
       Category3.Tasks.Add(new Task() { ID = 12, Name = "Website", Value = "" });  
       Category3.Tasks.Add(new Task() { ID = 13, Name = "Website", Value = "" });  
   
       // add to list  
       Categories.Add(Category1);  
       Categories.Add(Category2);  
       Categories.Add(Category3);  
   
       // return  
       return Categories;  
     }  
   
     public void Debug(string Value = "")  
     {  
       pnlDebug.Controls.Add(new LiteralControl(string.Format("<div>{0}</div>", Value)));  
     }  
   
     protected void btnSave_Click(object sender, EventArgs e)  
     {  
       // iterate outer repeater (categories)  
       foreach (RepeaterItem Category in lstCategories.Items)  
       {  
         // iterate inner repeater (tasks)  
         foreach (RepeaterItem Task in ((Repeater)(Category.FindControl("lstTasks"))).Items)  
         {  
           // get reference to controls  
           HiddenField lblID = (HiddenField)Task.FindControl("lblID");  
           HiddenField lblValue = (HiddenField)Task.FindControl("lblValue");  
           TextBox txtValue = (TextBox)Task.FindControl("txtValue");  
   
           // extract values  
           int ID = Convert.ToInt32(lblID.Value);  
           string ValueOriginal = lblValue.Value;  
           string ValueNew = txtValue.Text.Trim();  
   
           // debug  
           Debug(string.Format("ID={0}; ValueOriginal={1}; ValueNew={2}", ID, ValueOriginal, ValueNew));  
         }  
       }  
     }  
   
     protected void btnCancel_Click(object sender, EventArgs e)  
     {  
       Response.Redirect("default.aspx");  
       Response.End();  
     }  
   
     private class Category  
     {  
       public string Name { get; set; }  
       public List<Task> Tasks { get; set; }  
     }  
   
     private class Task  
     {  
       public int ID { get; set; }  
       public string Name { get; set; }  
       public string Value { get; set; }  
     }  
   }  
 }  

The btnSave_Click event is really where everything is happening. We're simply doing a foreach around the lstCategories repeater items. Then inside that loop we're iterating the lstTasks repeater items. It's inside this nested repeater (lstTasks) that we can get reference to the controls (lblID, lblValue, txtValue). From that point it's just a matter of casting the controls, extracting the values, and then using the values. In this case we're just outputting the values for debugging. But this is where you could use the ID and compare the original/new values to determine if you need to apply an update to the record, etc.

Click here to see a full working example of this demo.

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey

Sunday, June 14, 2015

SQL Code Generation Tool

The SQL Code Generation Tool is an open source project that I created to help automate the creation of stored procedures, model objects, and the data layer classes in the majority of the projects I'm involved in.

These scripts started in late 2010 when I simply got tired of manually coding VB.Net model classes. Over time, I added additional scripts to code gen the basic stored procedures, model classes, and data layer classes that I generally needed for each entity in my project. Initially the scripts only generated VB.Net code for the model/data layers; however, they now support both VB.Net and C#.

The scripts have went through 3 major rewrites/refactoring to get to their current version (v3.0) and now work with SQLCMD, which makes generating your files (procs, model, data) very simple.

Please keep in mind these scripts were written for a specific style of n-tier architecture, so they may or may not fit your needs out of the box. For most of the applications I work on there is a Web layer, Model layer, Data layer, and a backend SQL Server database (business/data).

These scripts more or less automated 90% of the routine tasks I would encounter after designing a database/table. I almost exclusively use these scripts to create the initial set of stored procedures, model classes (C#, VB.Net), and data layer classes (C#, VB.Net). Once generated I customize them as-needed to fit the project's needs (e.g. adding advanced filtering to stored procedures, adding derived properties to the model class, etc).

Head over to the GitHub repository for more information.

https://github.com/mpavey/sql-code-gen

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey

Friday, June 12, 2015

Using SQL Server SQLCMD and WinZip wzzip to backup and zip/encrypt a database

Here's an example of how to create a BAT file to backup a SQL Server database using SQLCMD and to zip the backup file using WinZip wzzip with AES256 encryption.
 @ECHO OFF  
   
 set server=(local)  
 set database=Sandbox  
 set bakfile=C:\Users\matt\Desktop\temp\backup.bak  
 set zipfile=C:\Users\matt\Desktop\temp\backup.zip  
 set targetfile=C:\Users\matt\Desktop\temp\backup\backup.zip  
 set password=Password123  
   
 cd C:\Program Files\Microsoft SQL Server\110\Tools\Binn  
 SQLCMD -E -S %server% -Q "BACKUP DATABASE %database% TO DISK = N'%bakfile%' WITH COPY_ONLY, NOFORMAT, INIT"  
   
 IF EXIST "%zipfile%" del /F "%zipfile%"  
   
 cd C:\Program Files\WinZip  
 wzzip "%zipfile%" "%bakfile%" -s%password% -ycAES256  
   
 IF EXIST "%bakfile%" del /F "%bakfile%"  
   
 move "%zipfile%" "%targetfile%"  

You could then use the BAT file to create a scheduled job to automate your backup process.

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey

Thursday, June 11, 2015

Shredding XML data (elements) using SQL Server

Here's a simple example of shredding XML data (elements) in SQL Server.
 -- variables  
 DECLARE @xml XML = '  
 <dinosaurs>  
  <d>  
   <name>Aachenosaurus</name>  
   <url>http://en.wikipedia.org/wiki/Aachenosaurus</url>  
  </d>  
  <d>  
   <name>Aardonyx</name>  
   <url>http://en.wikipedia.org/wiki/Aardonyx</url>  
  </d>  
  <d>  
   <name>Abdallahsaurus</name>  
   <url>http://en.wikipedia.org/wiki/Abdallahsaurus</url>  
  </d>  
  <d>  
   <name>Abelisaurus</name>  
   <url>http://en.wikipedia.org/wiki/Abelisaurus</url>  
  </d>  
  <d>  
   <name>Abrictosaurus</name>  
   <url>http://en.wikipedia.org/wiki/Abrictosaurus</url>  
  </d>  
  <d>  
   <name>Abrosaurus</name>  
   <url>http://en.wikipedia.org/wiki/Abrosaurus</url>  
  </d>  
  <d>  
   <name>Abydosaurus</name>  
   <url>http://en.wikipedia.org/wiki/Abydosaurus</url>  
  </d>  
  <d>  
   <name>Acanthopholis</name>  
   <url>http://en.wikipedia.org/wiki/Acanthopholis</url>  
  </d>  
  <d>  
   <name>Achelousaurus</name>  
   <url>http://en.wikipedia.org/wiki/Achelousaurus</url>  
  </d>  
  <d>  
   <name>Achillesaurus</name>  
   <url>http://en.wikipedia.org/wiki/Achillesaurus</url>  
  </d>  
 </dinosaurs>'  
   
 -- use common table expression to shred data  
 ;WITH ShreddedData (name, url) AS  
 (  
 SELECT  x.node.value('name[1]', 'VARCHAR(255)'),  
      x.node.value('url[1]', 'VARCHAR(255)')  
 FROM   @xml.nodes('dinosaurs/d') x(node)  
 )  
 SELECT * FROM ShreddedData;  

Shredding XML data (attributes) using SQL Server

Here's a simple example of shredding XML data (attributes) in SQL Server.
 -- variables  
 DECLARE @xml XML = '  
 <dinosaurs>  
  <d name="Aachenosaurus" url="http://en.wikipedia.org/wiki/Aachenosaurus" />  
  <d name="Aardonyx" url="http://en.wikipedia.org/wiki/Aardonyx" />  
  <d name="Abdallahsaurus" url="http://en.wikipedia.org/wiki/Abdallahsaurus" />  
  <d name="Abelisaurus" url="http://en.wikipedia.org/wiki/Abelisaurus" />  
  <d name="Abrictosaurus" url="http://en.wikipedia.org/wiki/Abrictosaurus" />  
  <d name="Abrosaurus" url="http://en.wikipedia.org/wiki/Abrosaurus" />  
  <d name="Abydosaurus" url="http://en.wikipedia.org/wiki/Abydosaurus" />  
  <d name="Acanthopholis" url="http://en.wikipedia.org/wiki/Acanthopholis" />  
  <d name="Achelousaurus" url="http://en.wikipedia.org/wiki/Achelousaurus" />  
  <d name="Achillesaurus" url="http://en.wikipedia.org/wiki/Achillesaurus" />  
 </dinosaurs>'  
   
 -- use common table expression to shred data  
 ;WITH ShreddedData (name, url) AS  
 (  
 SELECT  x.node.value('@name', 'VARCHAR(255)'),  
      x.node.value('@url', 'VARCHAR(255)')  
 FROM   @xml.nodes('dinosaurs/d') x(node)  
 )  
 SELECT * FROM ShreddedData;  

Tuesday, June 9, 2015

Using MVC Web API and SQL Server to create your own PayPal IPN Listener

Recently I was tasked with creating a PayPal IPN Listener, so I started with getting familiar with the PayPal Instant Payment Notification (IPN) documentation:

https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/

For this implementation I'm using a MVC Web API Controller (VB.Net) for the listener, along with some helper classes/extensions/methods. I'm also using a SQL Server 2012 table and stored procedure to create a notifications log, which allows you to:

1) Listen
2) Log
3) React

This is important because it allows the listener to do it's job quickly, and lets you focus on the processing later.

Let's start with our SQL Server database Notifications table.
 SET ANSI_NULLS ON  
 GO  
   
 SET QUOTED_IDENTIFIER ON  
 GO  
   
 SET ANSI_PADDING ON  
 GO  
   
 CREATE TABLE [dbo].[Notifications](  
      [NotificationID] [INT] IDENTITY(1,1) NOT NULL,  
      [Timestamp] [DATETIME] NOT NULL CONSTRAINT [DF_Notifications_Timestamp] DEFAULT (GETDATE()),  
      [Type] [VARCHAR](50) NOT NULL CONSTRAINT [DF_Notifications_Type] DEFAULT (''),  
      [IPAddress] [VARCHAR](25) NOT NULL CONSTRAINT [DF_Notifications_IPAddress] DEFAULT (''),  
      [UrlRequest] [VARCHAR](MAX) NOT NULL CONSTRAINT [DF_Notifications_UrlRequest] DEFAULT (''),  
      [UserAgent] [VARCHAR](MAX) NOT NULL CONSTRAINT [DF_Notifications_UserAgent] DEFAULT (''),  
      [Data] [VARCHAR](MAX) NOT NULL CONSTRAINT [DF_Notifications_Message] DEFAULT (''),  
      [Status] [VARCHAR](MAX) NOT NULL CONSTRAINT [DF_Notifications_Status] DEFAULT (''),  
      [Method] [VARCHAR](10) NOT NULL CONSTRAINT [DF_Notifications_Method] DEFAULT (''),  
      [Processed] [BIT] NOT NULL CONSTRAINT [DF_Notifications_Processed] DEFAULT ((0)),  
      [ProcessedDate] [DATETIME] NULL,  
      [Notes] [VARCHAR](MAX) NOT NULL DEFAULT (''),  
      [TransactionID] [VARCHAR](50) NOT NULL DEFAULT (''),  
      [TransactionType] [VARCHAR](50) NOT NULL DEFAULT (''),  
      [ItemName] [VARCHAR](128) NOT NULL DEFAULT (''),  
      [ItemNumber] [VARCHAR](128) NOT NULL DEFAULT (''),  
      [Option] [VARCHAR](200) NOT NULL DEFAULT (''),  
      [Email] [VARCHAR](100) NOT NULL DEFAULT (''),  
      [PaymentStatus] [VARCHAR](25) NOT NULL DEFAULT (''),  
  CONSTRAINT [PK_Notifications] PRIMARY KEY CLUSTERED   
 (  
      [NotificationID] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)  
 )  
   
 GO  
   
 SET ANSI_PADDING OFF  
 GO  

Next is the Notifications_Save stored procedure to save data in the Notifications table.
 SET ANSI_NULLS ON  
 GO  
   
 SET QUOTED_IDENTIFIER ON  
 GO  
   
 CREATE PROCEDURE [dbo].[Notifications_Save]  
 (  
      @NotificationID      INT,  
      @Type                VARCHAR(50),  
      @IPAddress           VARCHAR(25),  
      @UrlRequest          VARCHAR(MAX),  
      @UserAgent           VARCHAR(MAX),  
      @Data                VARCHAR(MAX),  
      @Status              VARCHAR(MAX),  
      @Method              VARCHAR(10),  
      @Processed           BIT,  
      @ProcessedDate       DATETIME  
 )  
 AS  
 BEGIN  
      -- check to see if record exists  
      IF EXISTS (SELECT NotificationID FROM dbo.Notifications WHERE NotificationID = @NotificationID)  
      BEGIN  
           -- update  
           UPDATE    dbo.Notifications  
           SET       Type = @Type,  
                     IPAddress = @IPAddress,  
                     UrlRequest = @UrlRequest,  
                     UserAgent = @UserAgent,  
                     Data = @Data,  
                     Status = @Status,  
                     Method = @Method,  
                     Processed = @Processed,  
                     ProcessedDate = @ProcessedDate  
           WHERE     NotificationID = @NotificationID  
      END  
      ELSE  
      BEGIN  
           -- insert  
           INSERT INTO dbo.Notifications  
                (Type, IPAddress, UrlRequest, UserAgent, Data, Status, Method, Processed, ProcessedDate)  
           VALUES  
                (@Type, @IPAddress, @UrlRequest, @UserAgent, @Data, @Status, @Method, @Processed, @ProcessedDate)  
    
           -- get identity value  
           SET @NotificationID = SCOPE_IDENTITY()  
      END  
    
      -- return value  
      RETURN @NotificationID  
 END  

Next we'll create the Notification model object in our MVC project.
 Public Class Notification  
   Public Property NotificationID As Integer = 0  
   Public Property Timestamp As DateTime = DateTime.MinValue  
   Public Property Type As String = String.Empty  
   Public Property IPAddress As String = String.Empty  
   Public Property UrlRequest As String = String.Empty  
   Public Property UserAgent As String = String.Empty  
   Public Property Data As String = String.Empty  
   Public Property Status As String = String.Empty  
   Public Property Method As String = String.Empty  
   Public Property Processed As Boolean = False  
   Public Property ProcessedDate As DateTime?  
 End Class  
   

Then we'll create the Notifications data layer class for calling the stored procedure.
 Imports Microsoft.Practices.EnterpriseLibrary.Data  
 Imports System.Data.Common  
   
 Public Class Notifications     
   Public Function Save(ByVal Notification As Model.Notification) As Integer  
     'variables  
     Dim DB As Database = New DatabaseProviderFactory().CreateDefault()  
     Dim ReturnValue As Integer = -1  
   
     'command  
     Using cmd As DbCommand = DB.GetStoredProcCommand("dbo.Notifications_Save")  
       'parameters  
       With Notification  
         DB.AddInParameter(cmd, "@NotificationID", DbType.Int32, .NotificationID)  
         DB.AddInParameter(cmd, "@Type", DbType.String, .Type)  
         DB.AddInParameter(cmd, "@IPAddress", DbType.String, .IPAddress)  
         DB.AddInParameter(cmd, "@UrlRequest", DbType.String, .UrlRequest)  
         DB.AddInParameter(cmd, "@UserAgent", DbType.String, .UserAgent)  
         DB.AddInParameter(cmd, "@Data", DbType.String, .Data)  
         DB.AddInParameter(cmd, "@Status", DbType.String, .Status)  
         DB.AddInParameter(cmd, "@Method", DbType.String, .Method)  
         DB.AddInParameter(cmd, "@Processed", DbType.Boolean, .Processed)  
         DB.AddInParameter(cmd, "@ProcessedDate", DbType.DateTime, IIf(.ProcessedDate.HasValue AndAlso Not .ProcessedDate.Equals(DateTime.MinValue), .ProcessedDate, DBNull.Value))  
       End With  
   
       'return value parameter  
       DB.AddParameter(cmd, "@ReturnValue", DbType.Int32, 4, ParameterDirection.ReturnValue, False, 0, 0, "@ReturnValue", DataRowVersion.Default, Nothing)  
   
       'execute query  
       DB.ExecuteNonQuery(cmd)  
   
       'get return value  
       ReturnValue = DB.GetParameterValue(cmd, "@ReturnValue")  
     End Using  
   
     'return value  
     Return ReturnValue  
   End Function  
 End Class  

Next we have a few helper classes/extensions/methods that we are going to be calling from the listener controller.

1) IsBlank (extension method)
2) ContainsValue (extension method)
3) Result (model object)
4) PostContentToUrl (shared method)

The IsBlank extension method seems trivial, especially for such a small example, but this is just one of the many extension methods that I keep in my utilities library to keep my code, clean, simple, and consistent.
   <Extension()> _  
   Public Function IsBlank(Value As String) As Boolean  
     Dim ReturnValue As Boolean = True  
   
     If Value IsNot Nothing Then  
       ReturnValue = Value.Trim().Length = 0  
     End If  
   
     Return ReturnValue  
   End Function  

The ContainsValue extension method is another one I keep in my library to to simplify the code so you don't have to deal with case sensitivity all over the place.
   <Extension()> _  
   Public Function ContainsValue(Value As String, CompareValue As String) As Boolean  
     Dim ReturnValue As Boolean = False  
   
     If Value IsNot Nothing AndAlso CompareValue IsNot Nothing Then  
       ReturnValue = Value.Trim().IndexOf(CompareValue.Trim(), StringComparison.OrdinalIgnoreCase) >= 0  
     End If  
   
     Return ReturnValue  
   End Function  

The Result model object is a simple helper class we're going to use when we post our data to the PayPal verification URL, which will allow us to return back a property indicating success or failure, as well as the response body.
 Public Class Result  
   Public Property Success As Boolean = False  
   Public Property Message As String = String.Empty    
 End Class  

Then we have our PostContentToUrl method, which was written with re-usability in mind. It accepts a URL and a request body. This allows you to use it for POST'ing data generically to any service.
   Public Shared Function PostContentToUrl(ByVal Url As String, ByVal Data As String) As Model.Result  
     'variables  
     Dim Result As New Model.Result  
   
     Try  
       'create web request  
       Dim MyRequest As WebRequest = WebRequest.Create(Url)  
       Dim ByteArray As Byte() = Encoding.UTF8.GetBytes(Data)  
   
       'properties  
       MyRequest.Method = "POST"  
       MyRequest.ContentType = "application/x-www-form-urlencoded"  
       MyRequest.ContentLength = ByteArray.Length  
   
       'get the request stream  
       Using DataStream As Stream = MyRequest.GetRequestStream()  
         'write the data to the request stream  
         DataStream.Write(ByteArray, 0, ByteArray.Length)  
   
         'close the stream object  
         DataStream.Close()  
       End Using  
   
       'response  
       Using MyResponse As HttpWebResponse = DirectCast(MyRequest.GetResponse(), HttpWebResponse)  
         Using MyReader As New StreamReader(MyResponse.GetResponseStream())  
           Result.Message = MyReader.ReadToEnd()  
           Result.Success = True  
         End Using  
       End Using  
     Catch ex As Exception  
       Result.Message = ex.Message  
     End Try  
   
     'return  
     Return Result  
   End Function  

Next we'll setup a BaseController class to expose several properties for the current HTTP request.
 Public Class BaseController   
   Inherits System.Web.Http.ApiController   
   
   Public ReadOnly Property QueryString() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.QueryString.ToString   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property IPAddress() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.UserHostAddress   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property UrlRequest() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.Url.AbsoluteUri   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property RawUrl() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.RawUrl   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property UrlReferrer() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.UrlReferrer.AbsoluteUri   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property UserAgent() As String   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.UserAgent   
     Catch ex As Exception   
      Return String.Empty   
     End Try   
    End Get   
   End Property   
     
   Public ReadOnly Property Crawler() As Boolean   
    Get   
     Try   
      Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.Browser.Crawler   
     Catch ex As Exception   
      Return False   
     End Try   
    End Get   
   End Property  

   Public ReadOnly Property Method() As String  
     Get  
       Try  
         Return DirectCast(Request.Properties("MS_HttpContext"), HttpContextBase).Request.HttpMethod  
       Catch ex As Exception  
         Return False  
       End Try  
     End Get  
   End Property  
 End Class  

Now that we've got all the core pieces in place let's take a look at the ListenerController, which is the simplest part of the entire process.
 Imports System.Net  
 Imports System.Web.Http  
 Imports System.IO  
   
 Namespace Controllers  
   <RoutePrefix("listener")>  
   Public Class ListenerController  
     Inherits BaseController  
   
     <HttpPost>  
     <Route("ipn")>  
     Public Function InstantPaymentNotification() As String  
       'variables  
       Dim Data As String = String.Empty  
       Dim Notification As New Model.Notification  
   
       'get data  
       Try  
         Data = New StreamReader(HttpContext.Current.Request.InputStream).ReadToEnd()  
       Catch ex As Exception  
         Data = String.Empty  
       End Try  
   
       'check for valid data  
       If Data.IsBlank Then  
         Return String.Empty  
       End If  
   
       'PayPal HTTP POSTs an IPN message to your listener that notifies it of an event.  
       'Your listener returns an empty HTTP 200 response to PayPal.  
       'Your listener HTTP POSTs the complete, unaltered message back to PayPal; the message must contain the same fields (in the same order) as the original message and be encoded in the same way as the original message.  
       'PayPal sends a single word back - either VERIFIED (if the message matches the original) or INVALID (if the message does not match the original).  
   
       'Every IPN message you receive from PayPal includes a User-Agent HTTP request header whose value is PayPal IPN ( https://www.paypal.com/ipn ).  
       'Do not use this header to verify that an IPN really came from PayPal and has not been tampered with.  
       'Rather, to verify these things, you must use the IPN authentication protocol outlined above.  
       If Not MyBase.UserAgent.ContainsValue("PayPal") Then  
         Return String.Empty  
       End If  
   
       'Before you can trust the contents of the message, you must first verify that the message came from PayPal.  
       'To verify the message, you must send back the contents in the exact order they were received and precede it with the command _notify-validate, as follows:  
       Dim VerifyUrl As String = IIf(Data.ContainsValue("test_ipn=1"), "https://www.sandbox.paypal.com/cgi-bin/webscr", "https://www.paypal.com/cgi-bin/webscr")  
       Dim VerifyData As String = String.Format("cmd=_notify-validate&{0}", Data)  
       Dim Result As Model.Result = Functions.PostContentToUrl(Url:=VerifyUrl, Data:=VerifyData)  
   
       'properties  
       Notification.Type = "IPN"  
       Notification.IPAddress = MyBase.IPAddress  
       Notification.UrlRequest = MyBase.UrlRequest  
       Notification.UserAgent = MyBase.UserAgent  
       Notification.Data = Data  
       Notification.Status = Result.Message  
       Notification.Method = MyBase.Method  
   
       'save  
       Using DB As New Data.Notifications  
         DB.Save(Notification)  
       End Using  
   
       'Important: After you have authenticated an IPN message (received a VERIFIED response from PayPal), you must perform these important checks before you can assume that the IPN is both legitimate and has not already been processed:  
       'Check that the payment_status is Completed.  
       'If the payment_status is Completed, check the txn_id against the previous PayPal transaction that you processed to ensure the IPN message is not a duplicate.  
       'Check that the receiver_email is an email address registered in your PayPal account.  
       'Check that the price (carried in mc_gross) and the currency (carried in mc_currency) are correct for the item (carried in item_name or item_number).  
       'Once you have completed these checks, IPN authentication is complete.  
       'Now, you can update your database with the information provided and initiate any back-end processing that's appropriate.  
   
       'return  
       Return String.Empty  
     End Function  
   End Class  
 End Namespace  

Because we've used <RoutePrefix("listener")> for the controller and <Route("ipn")> for the InstantPaymentNotification function, our IPN listener service would be exposed as follows:

http://yourdomain.com/listener/ipn

At this point you have an IPN listener that can:

1) Receive IPN notifications from PayPal
2) Respond back instantly to PayPal for the verification handshake
3) Log notification activity to the [Notifications] table in your database

This is the point in the process where you could go several different directions, and ultimately how you validate and process the data is going to be dependent on what types of transaction types (txn_type) you support and what types of services you are selling (e.g. products, subscriptions, etc).

With that said, here is an example of how you might do some data processing in SQL (e.g. a scheduled job that runs every minute).

We'll start with a Split table-valued function to help with splitting the IPN data vertically.
 SET QUOTED_IDENTIFIER ON  
 GO  

 CREATE FUNCTION [dbo].[Split]  
 (  
      @List          VARCHAR(MAX),  
      @Delimiter     VARCHAR(1)  
 )  
 RETURNS @Table TABLE  
 (   
      ID        INT IDENTITY(1,1),  
      Value     VARCHAR(MAX)  
 )  
 AS  
 BEGIN  
      -- loop through the list  
      WHILE (CHARINDEX(@Delimiter, @List) > 0)  
      BEGIN  
           -- add the value to the table  
           INSERT INTO @Table  
                (Value)  
                SELECT Value = LTRIM(RTRIM(SUBSTRING(@List, 1, CHARINDEX(@Delimiter, @List)-1)))  
   
           -- remove the value from the list  
           SET @List = SUBSTRING(@List, CHARINDEX(@Delimiter, @List) + 1, LEN(@List))  
      END  
   
      -- insert remaining value from the list  
      INSERT INTO @Table  
           (Value)  
           SELECT Value = LTRIM(RTRIM(@List))  
        
      -- return  
      RETURN  
 END  
   
 GO  

Next we'll use a SQL script to process any unprocessed notifications.
 /*  
 Important: After you have authenticated an IPN message (received a VERIFIED response from PayPal), you must perform these important checks before you can assume that the IPN is both legitimate and has not already been processed:  
 Check that the payment_status is Completed.  
 If the payment_status is Completed, check the txn_id against the previous PayPal transaction that you processed to ensure the IPN message is not a duplicate.  
 Check that the receiver_email is an email address registered in your PayPal account.  
 Check that the price (carried in mc_gross) and the currency (carried in mc_currency) are correct for the item (carried in item_name or item_number).  
 Once you have completed these checks, IPN authentication is complete.  
 Now, you can update your database with the information provided and initiate any back-end processing that's appropriate.  
 */  
   
 -- set nocount on  
 SET NOCOUNT ON;  
   
 -- if status is not verified mark it as processed and move on  
 UPDATE    dbo.Notifications  
 SET       Processed = 1,  
           ProcessedDate = GETDATE(),  
           Notes = 'Invalid'  
 WHERE     Type = 'IPN'  
 AND       Processed = 0  
 AND       Status != 'VERIFIED'  
   
 -- temp table to get a list of records that are being processed in this batch
 DECLARE @Batch TABLE  
 (  
      NotificationID     INT              NOT NULL DEFAULT 0  
 )  
   
 -- temp table to split out the IPN data vertically
 DECLARE @Details TABLE  
 (  
      BatchID            VARCHAR(50)      NOT NULL DEFAULT '',  
      NotificationID     INT              NOT NULL DEFAULT 0,  
      Property           VARCHAR(50)      NOT NULL DEFAULT '',  
      Value              VARCHAR(MAX)     NOT NULL DEFAULT ''  
 )  
   
 -- get unprocessed notifications that are verified and save in temp table  
 INSERT INTO @Batch  
   (NotificationID)  
      SELECT       NotificationID  
      FROM         dbo.Notifications  
      WHERE        Type = 'IPN'  
      AND          Processed = 0  
      AND          Status = 'VERIFIED'  
      ORDER BY     NotificationID  
   
 -- declare variables for cursor  
 DECLARE @NotificationID    INT           = 0  
 DECLARE @Data              VARCHAR(MAX)  = ''  
   
 -- declare cursor  
 DECLARE MyCursor CURSOR LOCAL FAST_FORWARD  
 FOR  
      SELECT       n.NotificationID,  
                   n.Data  
      FROM         dbo.Notifications n  
      JOIN         @Batch x ON n.NotificationID = x.NotificationID  
      ORDER BY     n.NotificationID  
   
 -- open cursor  
 OPEN MyCursor  
   
 -- get the first result  
 FETCH NEXT FROM MyCursor INTO @NotificationID, @Data  
   
 -- loop through results  
 WHILE @@FETCH_STATUS = 0  
 BEGIN       
      -- variables  
      DECLARE @BatchID    VARCHAR(50) = FORMAT(GETDATE(), 'yyyyMMddHHmmssfffffff') + dbo.GetRandomString(5)  
        
      -- split IPN data vertically
      INSERT INTO @Details  
           (BatchID, NotificationID, Property, Value)  
           SELECT    BatchID = @BatchID,  
                     @NotificationID,  
                     Property = (SELECT Value FROM dbo.Split(Value, '=') WHERE ID = 1),  
                     Value = (SELECT dbo.DecodeValue(Value) FROM dbo.Split(Value, '=') WHERE ID = 2)  
           FROM      dbo.Split(@Data, '&')  
   
      -- fetch the next record  
      FETCH NEXT FROM MyCursor INTO @NotificationID, @Data  
 END  
   
 -- close cursor  
 CLOSE          MyCursor  
 DEALLOCATE     MyCursor  
   
 -- extract core information that we are interested in for our payment processing
 -- txn_id, txn_type, item_name, item_number, option_selection1, custom, payment_status
 UPDATE    n  
 SET       n.TransactionID = ISNULL(x.txn_id, ''),  
           n.TransactionType = ISNULL(x.txn_type, ''),  
           n.ItemName = ISNULL(x.item_name, ''),  
           n.ItemNumber = ISNULL(x.item_number, ''),  
           n.[Option] = ISNULL(x.option_selection1, ''),  
           n.Email = ISNULL(x.custom, ''),  
           n.PaymentStatus = ISNULL(x.payment_status, '')  
 FROM      @Details d  
 PIVOT     (  
                MAX(d.Value)  
                FOR Property IN (txn_id, txn_type, item_name, item_number, option_selection1, custom, payment_status)  
           )     AS x  
 JOIN      dbo.Notifications n ON x.NotificationID = n.NotificationID  
   
 -- process non-payments  
 UPDATE    n  
 SET       n.Processed = 1,  
           n.ProcessedDate = GETDATE(),  
           n.Notes = 'Non-payment'  
 FROM      dbo.Notifications n  
 JOIN      @Batch x ON n.NotificationID = x.NotificationID  
 WHERE     n.Processed = 0  
 AND       (LEN(n.TransactionID) = 0 OR LEN(n.PaymentStatus) = 0)  
   
 -- process duplicate transactions  
 UPDATE    n  
 SET       n.Processed = 1,  
           n.ProcessedDate = GETDATE(),  
           n.Notes = 'Duplicate'  
 FROM      dbo.Notifications n  
 JOIN      @Batch x ON n.NotificationID = x.NotificationID  
 WHERE     n.Processed = 0  
 AND       n.TransactionID IN (SELECT y.TransactionID FROM dbo.Notifications y WHERE y.Processed = 1)  
   
 -- process non-completed payments  
 UPDATE    n  
 SET       n.Processed = 1,  
           n.ProcessedDate = GETDATE(),  
           n.Notes = 'Unknown payment status'  
 FROM      dbo.Notifications n  
 JOIN      @Batch x ON n.NotificationID = x.NotificationID  
 WHERE     n.Processed = 0  
 AND       n.PaymentStatus != 'Completed'  
   
 -- process completed payments  
 UPDATE    n  
 SET       n.Processed = 1,  
           n.ProcessedDate = GETDATE(),  
           n.Notes = 'Completed'  
 FROM      dbo.Notifications n  
 JOIN      @Batch x ON n.NotificationID = x.NotificationID  
 WHERE     n.Processed = 0  
 AND       n.PaymentStatus = 'Completed'  

This is the point where if you have new "completed" payments you could trigger other workflow items in your system, such as email notifications to send a license key, or renewing a subscription, or otherwise giving access to the product/service to the payee.

Remember this is just a starting point and your specific data processing implementation will vary based on your needs.

Matt Pavey is a Microsoft Certified software developer who specializes in ASP.Net, VB.Net, C#, AJAX, LINQ, XML, XSL, Web Services, SQL, jQuery, and more. Follow on Twitter @matthewpavey