Sunday, July 6, 2014

Creating a simple address finder using google place autocomplete

There are a number old asp.net forms applications out there with data entry forms. If by any  chance you are bound to use a web system with lots of user demographic information capture then you are left with no choice but to type the details out. Having stated so there are a number of ways you can add value to an existing application and improve user experience without changing much of the existing data capturing fields and associated logic. Using location based services will be one of the quick wins and this post will detail about  creating an address finder user control.

Googles’ Place Autocomplete is used in this post and you do not need to go through the hassle of getting specific API key for non-commercial or low usage scenario. Read google place autocomplete documentation for more information. I have existing asp.net web application which uses address entry fields as seen below
Same address fields are sitting in the application in different areas with different validation criteria’s and logic wired to these fields.  One of the easy ways to bring user friendliness is to introduce an auto address populate control shown as below.

Once the user selects the recommendations the address data will be populated back onto the existing address fields. Here I am not modifying the existing controls or code , instead adding some additional capability to populate address fields faster. This approach mitigates the risk of breaking any existing feature and avoided much of regression test.

We can now look how to achieve this in the following steps  
  • Include the following javascript in masterpage of the application
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false&libraries=places&language=en-AU">
</script>

  • Create a user control AddressAutoComplete.ascx with following mark up
<div>
Address Search<asp:TextBox ID="AddressTextBox" runat="server" Width="450" />
</div>
  • Add the following javascript code to the user control mark up page(we are using query and the following code works on old jquery versions as well)
<script type="text/javascript">
$(function () {
 if (typeof google == 'undefined' || typeof google.maps.places == 'undefined') {
   //Make Sure the Library has loaded. Else Hide the autocomplete Box.
    $("#<%=AddressAutoFillRow.ClientID %>").hide();
  } else {
      //Setting up Auto Complete for Address
  var residentialAutocomplete = new google.maps.places
                                   .Autocomplete($('#<%=AddressTextBox.ClientID %>')[0], {});
  //Australia biased results
  var bounds = new google.maps.LatLngBounds(new google.maps.LatLng(-44.21370990970204,      110.7421875), new google.maps.LatLng(-9.188870084473393, 154.6435546875));

  residentialAutocomplete.setBounds(bounds);
  google.maps.event.addListener(residentialAutocomplete, 'place_changed', function () {

     var place = residentialAutocomplete.getPlace();
     var $data = new Array();
     for (var i = 0; i < place.address_components.length; i++) {
       $data[place.address_components[i].types[0]] = place.address_components[i].long_name;
     }
      var st = '';
      if ($data != undefined) {
          if ($data['street_number'] != undefined) st = $data['street_number'];
          if ($data['route'] != undefined) st += " " + $data['route'];
          if ($data['subpremise'] != undefined) st = $data['subpremise'] + " / " + st;
      }
      $('#<%=this.AddressLine1ClientId %>').val(st);
      $('#<%= this.CityClientId %>').val($data['locality']);
      if ($("#<%=this.CountryClientId %> option:contains('"+$data['country']+"')").length>0){
          $('#<%=this.CountryClientId %> option').filter(function () { 
           return ($(this).text() == $data['country']); }).attr('selected', true);
      } else {
               $('#<%=this.CountryClientId %>').val("");
         }
        $('#<%=this.StateClientId %>').val($data['administrative_area_level_1']);
        $('#<%=this.PostCodeClientId %>').val($data['postal_code']);
      });
     }
   });
</script>

  •  Code behind will look as follows and “ViewStateProperty” attribute from my earlier post is used to maintain state for properties in view state  

public partial class AddressAutoComplete : System.Web.UI.UserControl
{

[ViewStateProperty]
        public string AddressLine1ClientId { get; set; }
        [ViewStateProperty]
        public string CityClientId { get; set; }
        [ViewStateProperty]
        public string StateClientId { get; set; }
        [ViewStateProperty]
        public string PostCodeClientId { get; set; }
        [ViewStateProperty]
        public string CountryClientId { get; set; }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            if (!IsPostBack)
            {
                this.AddressLine1ClientId = CityClientId = StateClientId 
                  = PostCodeClientId = CountryClientId = string.Empty;
            }
        }
}

Now the control is ready to use in web pages; now we do that by registering control on page and setting the ClientId properties of the user control
<%@ Register Src="~/AddressAutoComplete.ascx" TagName="AddressSearch" TagPrefix="cc" %>
 <cc:AddressSearch ID="AddressAutoCompleteControl" runat="server" />
[the old mark up for address control sits here] 

In the code behind file aspx.cs file
protected void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack)
    {
     AddressAutoCompleteControl.AddressLine1ClientId = txtAddress.ClientID;
     AddressAutoCompleteControl.CityClientId = txtCity.ClientID;
     AddressAutoCompleteControl.StateClientId = txtState.ClientID;
     AddressAutoCompleteControl.PostCodeClientId = txtPostCode.ClientID;
     AddressAutoCompleteControl.CountryClientId = ddCountry.ClientID;
   }
}

Saturday, March 8, 2014

Creating concatenated delimited string from a SQL result set and avoid character encoding when using “FOR XML PATH”

There are a number of ways of getting a column result set into a single delimited string. If it is a function then you can use single variable assigning method or STUFF tsql function. The STUFF function in conjunction with FOR XML PATH code is the most easy usage if you have to use as an inner query. But it automatically html encodes string in certain cases (see case 2 bad example) .

Lets look at a simple exaple of  number of country names

Case 1. Variable based solution 
declare @delimitedCountryName varchar(max)
set @delimitedCountryName=''
select @delimitedCountryName+=case when len(@delimitedCountryName) > 0 then +','+ CountryName else CountryName end
FROM Country

select @delimitedCountryName

Case 2. The XML path and stuff system function based solution
//BAD: because if country name contains special characters then it will be automatically html encoded (e.g. if country name is 'papua & new guinea' it will be shown as    'papua &amp; new guinea' 
select stuff(
(select ', ' + countryname
from Country
for xml path('')
)
, 1, 1, '') as delimitedCountryName;


// GOOD approach (either use case 1 or below option)
select stuff(
(select ', ' + countryname
from Country
for xml path(''), root('MyString'), type
).value('/MyString[1]','varchar(max)')

Monday, January 13, 2014

Creating PDF documents from webpage response using ABCpdf

ABCpdf is a handy third party (paid) tool to convert any form of documents to PDF files. It is also observed that in number of instances developers being asked to generate PDF files based upon a web page response. Here is the sample code to implement PDF generation from a target aspx file response.

In this example I am demonstrating how to effectively use ABCpdf in generating pdf files from an aspx page response; it could be any url that spits out html data. We can create GeneratePdf method in the asp.net web application and leverage HttpResponse Object to write out the pdf.

Let’s assume the url is http://loaduserdetails.aspx/id=1 and we our api once developed will allow us to call the code: GeneratePDF(“is http://loaduserdetails.aspx/id=1”);

public void GeneratePDF(string url)
{
  var pdfGenerator = new PdfGenerator();
  byte[] dataBytes = pdfGenerator .GetPdfDataBytes(pageUrl);
  Response.ContentType = "application/pdf";
   Response.AddHeader("content-disposition", "inline; filename=member_profile.pdf");
  Response.AddHeader("content-length", theData.Length.ToString());
  // ensure that the response is not cached. That would defeat the whole purpose
  Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(-1));
  Response.AddHeader("Cache-Control", " no-store, no-cache   issue fix
  Response.Cache.SetNoStore();
  Response.BinaryWrite(dataBytes);
  Response.End();
}
The PdfGenerator class will have dependency on ABCpdf and for illustration purposes I am using ABCpdf version8. The resulting PdfGenerator class code will look as below
    using WebSupergoo.ABCpdf8;
    public class PdfGenerator
    {
        private Doc _pdFdoc;
        public PdfGenerator()
        {
            this._pdFdoc = new Doc();
        }

       public byte[] GetPdfDataBytes(string aspxPageURL)
        { 
            //disable page caching
            _pdFdoc.HtmlOptions.PageCacheEnabled = false;
            _pdFdoc.HtmlOptions.UseNoCache = true;
            _pdFdoc.FontSize = 12;
            //load the document
            ifthis.LoadDocument(aspxPageURL))
            {
                // load the footer
                AddDocumentFooter();
            }

         var dataArray= _pdFdoc.GetData();
        _pdFdoc.Dispose(); 
        return dataArray;        
        }   
        private bool LoadDocument(string theUrl)
        {
            bool success=false;
            var pDdoc = new Doc();
            pDdoc.Rect.Inset(50, 50);
            try
            {
                var docPageCount = pDdoc.AddImageUrl(theUrl, true, 800, true);
                while (true)
                {
                    if (!pDdoc.Chainable(docPageCount))
                    {
                        break;
                    }
                    pDdoc.Page = pDdoc.AddPage();
                    docPageCount = pDdoc.AddImageToChain(docPageCount);
                }
                _pdFdoc.Append(pDdoc);
                success=true;
            }
            catch (Exception err)
            {
                //handle error
                _pdFdoc.AddHtml("<b>Unable to render page</b>");
            }
            finally
            {
                pDdoc.Dispose();
            }
            return success;
        } 

  private void AddDocumentFooter()
        {
            _pdFdoc.Rect.String = "30 790 600 30";
            _pdFdoc.Rect.Position(0, 30);
            _pdFdoc.HPos = 1.0; // Align text right
            _pdFdoc.FontSize = 10;
            for (var i = 1; i <= _pdFdoc.PageCount; i++)
            {
                _pdFdoc.Page = i;
                _pdFdoc.PageNumber = i;
                _pdFdoc.Font = _pdFdoc.EmbedFont("Tahoma");                
                _pdFdoc.AddText("Document Generated by MyApplication  Page " 
                                                                    + i + " of " + _pdFdoc.PageCount);                
                _pdFdoc.Flatten();
            }

        }

    }