ASP.net Web Application reverting to Website

This is defiantly a “Note To Self” post.

I still don’t know why it happens, but occasionally an ASP.net Web App turns itself into an ASP.net web site.

If this happens try the following:

  • Remove the .webinfo file
  • Remove any reference to the project in IIS

Simple Google Analytics ASP.net control

Google Analytics is a great tool for analysing website traffic and goal conversion. When using asp.net, Master Pages, can make implementing this easy. However if there are multiple Master Pages or many pages that aren’t derived from master pages a simple control to drop the Google scripts on a page.

The following control provides a simple way to add Google Analytics scripts to a page. Furthermore it is customisable if the need arises to override the page that is tracked, which is handy when doing goal conversion with an ASP.net page which post backs to itself.

Note that the control inherits from the control class. This ensures there are no additional tags rendered other than the script tags.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MyControls
{
    /// <summary>
    /// Web control to put basic Google Analytics on a page
    /// </summary>
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:GenericGoogleAnalytics runat=server></{0}:GenericGoogleAnalytics>")]
    public class GenericGoogleAnalytics : Control
    {
        
        /// <summary>
        /// Google Identifier
        /// </summary>
        [Bindable(true)]
        [Category("Initialisation")]
        [DefaultValue("")]
        [Localizable(true)]
        public string GoogleID
        {
            get
            {
                String s = (String)ViewState["GoogleID"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["GoogleID"] = value;
            }
        }

        /// <summary>
        /// Page to track if not current page.
        /// </summary>
        /// <remarks>
        /// <para>This is generaly used for pages after a form is submitted.</para>
        /// <para>A ficticious url is set up in Google Analytics and used here.</para>
        /// </remarks>
        [Bindable(true)]
        [Category("Initialisation")]
        [DefaultValue("")]
        [Localizable(true)]
        [Description("Page to track if not current page")]
        public string TrackPageview
        {
            get
            {
                String s = (String)ViewState["TrackPageview"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["TrackPageview"] = value;
            }
        }

        /// <summary>
        /// Renders control to page
        /// </summary>
        /// <param name="output">Output writer</param>
        protected override void Render(HtmlTextWriter output)
        {
              //Prototype google script           
              string googleScript = @"  
                    var _gaq = _gaq || [];
                    _gaq.push(['_setAccount', '{0}']);
                    _gaq.push({1});

                    (function() {{
                        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
                        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
                        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
                    }})();";
            
            output.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
            output.RenderBeginTag(HtmlTextWriterTag.Script);           

            output.Write(string.Format(googleScript, GoogleID, GetTracking()));

            output.RenderEndTag();
        }

        //Make Sure Google ID supplied
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            if (string.IsNullOrEmpty(GoogleID))
                throw new HttpException(string.Format("Google ID of {0} can not be blank", this.ID));
        }

        private string GetTracking()
        {
            string retVal = "['_trackPageview']";
            if (!string.IsNullOrEmpty(TrackPageview))
                retVal = string.Format("['_trackPageview','{0}']", TrackPageview);

            return retVal;
        }
    }
}

After adding the control to the Visual Studio tool box here is an example of usage demonstrating using a fictitious for goal tracking.

ASPX

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GATest.aspx.cs" Inherits="HubbCom.GATest" %>

<%@ Register Assembly="MyControls" Namespace="MyControls" TagPrefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <cc1:GenericGoogleAnalytics ID="GA1" GoogleID="UA-7419588-2" runat="server">
    </cc1:GenericGoogleAnalytics>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Button ID="Button1" runat="server" Text="Button" />
    
    </div>
    </form>
</body>
</html>

.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MyNameSpace
{
    public partial class GATest : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
          
	    //Set page tracker for ficticious url
	    if (IsPostBack)
            {
                GA1.TrackPageview = "/thankyou.aspx";
            }
        }
    }
}

Possible improvements could be made by having a default Google ID stored in the web.config file and make the bounding script tags optional

Conditionally Populating Select Lists Client Side

When using classic ASP when I needed to populate a select list client side based on an action such as a checking a check box radio button or some other client action, I would create some client side JavaScript objects on the page and an array of these objects. When the user performed the appropriate action I would walk the array pulling the required data into the select list.

When I first started developing in ASP.net I basically continued using this pattern, battling Request Validation errors as I had changed the expected values in the drop down list. This days AJAX is a popular method of performing this kind of action, however I like to reduce repeated calls to the server if I can avoid it.

I’ve recently flipped the pattern on its head. Now I completely populate the select list server side, then on page load, client side, use jQuery to make a clone of the option, and empty the select list (or set it to another default state. Then when the user performs the appropriate action append options from the cloned list to the original list.

The following example is modified from real life form, the user selects an event an a list of marketing sources for that event is populated in the select list. The sources and events are stored in a jSON variable.

//Use a nice wide scope
var origSelSourceList;
var selSourceList;

//jSON string to hold the details
var details = [{
    "EventID": 2738,
    "Detail": "Dee Why - Wednesday, 10 August 2011 - 6:30 PM",
    "Sources": [393, 1101, 1135, 1106, 24, 1128, 1158, 1150, 863, 955, 1470, 1130, 1139, 1137, 1141]
}, {
    "EventID": 2776,
    "Detail": "Surfers Paradise - Sunday, 18 September 2011 - 11:00 AM",
    "Sources": [393, 1101, 1135, 1106, 1158, 1150, 863, 955, 1470, 1130, 1139, 1137, 1141]
}, {
    "EventID": 2782,
    "Detail": "Scarborough - Saturday, 24 September 2011 - 11:00 AM",
    "Sources": [393, 1101, 1135, 1106, 1158, 1150, 863, 955, 1470, 1130, 1139, 1137, 1141]
}];


$(document).ready(function () {
    selSourceList = $("#clientForm_Source"); //Cache the select list for later
    origSelSourceList = $("#clientForm_Source").clone(); // Make a cloned copy
    selSourceList.find('option').remove().end().append('<option value="-1">Select Event</option>');

    //Our radio buttons that cause the list
    //to be populated are in a div with an id of "forPop"
    $("#forPop input:radio").click(function () {

        //Add a first default
        selSourceList.find('option').remove().end().append('<option value="-1">Please Select</option>');

        //Event ID to find the sources for
        var id = $(this).val();

        //Find the event
        for (var i = 0; i < details.length; i++) {
            if (details[i].EventID == id) {

                //Get source for the event 
                for (var j = 0; j < details[i].Sources.length; j++) {
                    //Find and Clone the option from our cloned copy of the select list                             
                    selSourceList.append($(origSelSourceList).find('option[value="' + details[i].Sources[j] + '"]').clone());
                }
                break; // Found our event so stop looping
            }
        }
    });

});

The beauty of this technique is that it works nicely with ASP.net’s request validation, but can be used with any server side technology

Referencing External Script Libraries

Google’s CDN is a very powerful way of accessing JavaScript libraries like jQuery, Mootools etc. This is a great article on why you should be using a CDN for this accessing these kind of libraries:3 reasons why you should let Google host jQuery for you . The one problem is dealing with them when your page is accessed via https and the “mixed content” warning the browser can throw.

Fortunately there is a very simple solution:

<script type="text/javascript"  src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>

Notice the complete lack of http or https, this is not a typo, it basically retrieves the script using the protocol the page was called with! This works particularly well in template scenarios like asp.net master pages.

Validating Checked Controls in ASP.net

This is one that pretty much every ASP.net web forms developer will come across every now and then.

What to do when you have a bunch of  check boxes or radio buttons in some kind  of common container: Table, list, even a check box list, etc, and you need to make sure at least one is checked.

One way to handle this is to use a custom validator:

<asp:CustomValidator 
     ID="custEventVal" 
     runat="server" 
     ErrorMessage="Please Select An Event"
     ClientValidationFunction="ValidateEvents" 
     EnableClientScript="true" 
     Display="Dynamic" 
     onservervalidate="custEventVal_ServerValidate">
</asp:CustomValidator>

With client side validation:

//Requires jQuery
//#SelEvent is the id of our containing element
//in this case we are checking for radio buttons
function ValidateEvents(sender, args) {
    args.IsValid = ($("#SelEvent input:radio:checked").length > 0);
}

and server side:

/// <summary>Ensure radio button has been selected</summary>
/// <param name="source"></param>
/// <param name="args"></param>
protected void custEventVal_ServerValidate(object source, ServerValidateEventArgs args)
{
    int eventID;
    //in this instance our radio buttons are set up with the name
    //rdoEvents
    args.IsValid = (!String.IsNullOrEmpty(Request.Form["rdoEvent"]));
}

This will work on when the form is submitted but the validation error will still remain regardless of if one on the controls is checked. To resolve this use jQuery to wire up ValidatorHookupControlID, which is provided by the ASP.net validation frame work.

//#SelEvent is the id of our containing element
//in this case we are checking for radio buttons
$(document).ready(function() {
   $("#SelEvent input:radio").each(function() {
       ValidatorHookupControlID($(this).attr('id'), document.all["<%= custEventVal.ClientID %>"]);
   });
 });