Monday, September 17, 2012

Enabling unobtrusive validation from scratch in ASP.Net 4.5 webforms

Download the code of this article

I was playing with Visual Studio 2012 specifically with the new Validation features and I found they work great, specially the new unobtrusive validation, then I tried to enable this kind of validation on a new Empty Web Application, and I found that this is not out-of-the-box, you need to make some configurations to your Web Application.

There are three ways to enable the unobtrusive validation on a Web Application:

Via the web.config file


<configuration>  
  <appsettings>  
   <add key="ValidationSettings:UnobtrusiveValidationMode" value="WebForms">  
  </add></appsettings>  
 </configuration>  

Via the Global.asax file

protected void Application_Start(object sender, EventArgs e)
{
   ValidationSettings.UnobtrusiveValidationMode = UnobtrusiveValidationMode.None;
}

On each page:
protected void Page_Load(object sender, EventArgs e)
{
   this.UnobtrusiveValidationMode = System.Web.UI.UnobtrusiveValidationMode.WebForms;
}

To disable the unobtrusive validation set the UnobtrusiveValidationMode property to None

Unobtrusive validation is actually enabled by default in ASP.Net 4.5.

We'll start with a simple example, create an Empty Web Application and add a MasterPage called Site.master and a content page for this master called Default.aspx.

Add the following code to the Default.aspx file:


    <asp:TextBox runat="server" ID="txt" />
    <asp:RequiredFieldValidator ErrorMessage="txt is required" ControlToValidate="txt" runat="server" Text="*" Display="Dynamic" />
    <asp:Button Text="Send info" runat="server" />



If you try to run a simple ASPX page using a validator, the following exception will be thrown:
"WebForms UnobtrusiveValidationMode requires a ScriptResourceMapping for 'jquery'. Please add a ScriptResourceMapping named jquery(case-sensitive)".
Before fixing this, let's disable the unobtrusive validation to see the result.

On the page:

protected void Page_Load(object sender, EventArgs e)
{
   this.UnobtrusiveValidationMode = System.Web.UI.UnobtrusiveValidationMode.None;
}

Now run the page and the validation will work as it used to work in ASP.Net 4.0

If you examine the rendered HTML, you will see that the gross inline script is rendered:



<script type="text/javascript">
//<![CDATA[
var Page_Validators =  new Array(document.getElementById("ContentPlaceHolder1_ctl00"));
//]]>
</script>

<script type="text/javascript">
//<![CDATA[
var ContentPlaceHolder1_ctl00 = document.all ? document.all["ContentPlaceHolder1_ctl00"] : document.getElementById("ContentPlaceHolder1_ctl00");
ContentPlaceHolder1_ctl00.controltovalidate = "ContentPlaceHolder1_txt";
ContentPlaceHolder1_ctl00.errormessage = "txt is required";
ContentPlaceHolder1_ctl00.display = "Dynamic";
ContentPlaceHolder1_ctl00.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid";
ContentPlaceHolder1_ctl00.initialvalue = "";
//]]>
</script>


<script type="text/javascript">
//<![CDATA[

var Page_ValidationActive = false;
if (typeof(ValidatorOnLoad) == "function") {
    ValidatorOnLoad();
}

function ValidatorOnSubmit() {
    if (Page_ValidationActive) {
        return ValidatorCommonOnSubmit();
    }
    else {
        return true;
    }
}
        
document.getElementById('ContentPlaceHolder1_ctl00').dispose = function() {
    Array.remove(Page_Validators, document.getElementById('ContentPlaceHolder1_ctl00'));
}
//]]>
</script>



Now let's re-enable the unobtrusive validation. In order to fix the previous exception, we need to install the following Nuget packages: (I like to install jQuery first to get the latest version, although this is not required.)



  1. jQuery
  2. ASPNET.ScriptManager.jQuery
  3. Microsoft.AspNet.ScriptManager.MSAjax
  4. Microsoft.AspNet.ScriptManager.WebForms

At this point, if you run the application again, the exception will be gone =) how cool eh?. This is because the following Nuget packages automatically register the scripts needed with the ScriptManager control.

Let's examine the code added by these Nuget packages using ILSpy:

  • AspNet.ScriptManager.jQuery
    
        public static class PreApplicationStartCode
        {
            public static void Start()
            {
                string str = "1.8.1";
                ScriptManager.ScriptResourceMapping.AddDefinition("jquery", new ScriptResourceDefinition
                {
                    Path = "~/Scripts/jquery-" + str + ".min.js",
                    DebugPath = "~/Scripts/jquery-" + str + ".js",
                    CdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + str + ".min.js",
                    CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + str + ".js",
                    CdnSupportsSecureConnection = true,
                    LoadSuccessExpression = "window.jQuery"
                });
            }
        }
    
  • Microsoft.AspNet.ScriptManager.MSAjax
    public static void Start()
    {
        ScriptManager.ScriptResourceMapping.AddDefinition("MsAjaxBundle", new ScriptResourceDefinition
        {
            Path = "~/bundles/MsAjaxJs",
            CdnPath = "http://ajax.aspnetcdn.com/ajax/4.5/6/MsAjaxBundle.js",
            LoadSuccessExpression = "window.Sys",
            CdnSupportsSecureConnection = true
        });
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjax.js", "window.Sys && Sys._Application && Sys.Observer");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxCore.js", "window.Type && Sys.Observer");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxGlobalization.js", "window.Sys && Sys.CultureInfo");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxSerialization.js", "window.Sys && Sys.Serialization");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxComponentModel.js", "window.Sys && Sys.CommandEventArgs");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxNetwork.js", "window.Sys && Sys.Net && Sys.Net.WebRequestExecutor");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxHistory.js", "window.Sys && Sys.HistoryEventArgs");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxWebServices.js", "window.Sys && Sys.Net && Sys.Net.WebServiceProxy");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxTimer.js", "window.Sys && Sys.UI && Sys.UI._Timer");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxWebForms.js", "window.Sys && Sys.WebForms");
        PreApplicationStartCode.AddMsAjaxMapping("MicrosoftAjaxApplicationServices.js", "window.Sys && Sys.Services");
    }
    private static void AddMsAjaxMapping(string name, string loadSuccessExpression)
    {
        ScriptManager.ScriptResourceMapping.AddDefinition(name, new ScriptResourceDefinition
        {
            Path = "~/Scripts/WebForms/MsAjax/" + name,
            CdnPath = "http://ajax.aspnetcdn.com/ajax/4.5/6/" + name,
            LoadSuccessExpression = loadSuccessExpression,
            CdnSupportsSecureConnection = true
        });
    }
    
  • Microsoft.AspNet.ScriptManager.WebForms
    
    public static void Start()
    {
        ScriptManager.ScriptResourceMapping.AddDefinition("WebFormsBundle", new ScriptResourceDefinition
        {
            Path = "~/bundles/WebFormsJs",
            CdnPath = "http://ajax.aspnetcdn.com/ajax/4.5/6/WebFormsBundle.js",
            LoadSuccessExpression = "window.WebForm_PostBackOptions",
            CdnSupportsSecureConnection = true
        });
    }
    

As you can see these Nuget packages automatically register the scripts using the ScriptManager object (besides installing the required JavaScript files)

Run the application and examine the rendered HTML. You will note that it's much cleaner now, in this case the inline script has been moved to an external file that can be rendered using bundles to increase performance. The rendered script looks like:

<script src="Scripts/WebForms/MsAjax/MicrosoftAjaxWebForms.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.8.1.js" type="text/javascript"></script>

Much better right?. Notice how ASP.Net used HTML5 custom attributes:


<input name="ctl00$ContentPlaceHolder1$txt" type="text" id="ContentPlaceHolder1_txt" />
    <span data-val-controltovalidate="ContentPlaceHolder1_txt" data-val-errormessage="txt is required" data-val-display="Dynamic" id="ContentPlaceHolder1_ctl00" data-val="true" data-val-evaluationfunction="RequiredFieldValidatorEvaluateIsValid" data-val-initialvalue="" style="display:none;">*</span>
    <input type="submit" name="ctl00$ContentPlaceHolder1$ctl01" value="Send info" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;ctl00$ContentPlaceHolder1$ctl01&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" />

The data-val-* are custom attributes used by the unobtrusive validation engine

If you click the button to trigger the validation you will be pleased to see that it works as expected...but we are not done yet =/

The settings we have applied won't work if you intend to use an UpdatePanel control (yeah the evil UpdatePanel again...). This is because this control requires a ScriptManager control on the page (or MasterPage) and we do not have any yet. So let's add a simple ScriptManager control to the master page and see what happens. Add the following code to the Site.master page right under the <form...


        <asp:ScriptManager runat="server" ID="scriptManager">
        </asp:ScriptManager>

Run the page again and fire the validation... oops... the client validation has gone =( We only have server validation. I'm not sure why this happens but my best guess is that the just added ScriptManager control is overriding our code settings.

To fix it, change the declaration of the ScriptManager control on the Site.master page to:


    <asp:ScriptManager runat="server" ID="scriptManager1">
        <Scripts>
            <asp:ScriptReference Name="MsAjaxBundle" />
            <asp:ScriptReference Name="jquery" />
            <asp:ScriptReference Name="WebForms.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebForms.js" />
            <asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebUIValidation.js" />
            <asp:ScriptReference Name="MenuStandards.js" Assembly="System.Web" Path="~/Scripts/WebForms/MenuStandards.js" />
            <asp:ScriptReference Name="GridView.js" Assembly="System.Web" Path="~/Scripts/WebForms/GridView.js" />
            <asp:ScriptReference Name="DetailsView.js" Assembly="System.Web" Path="~/Scripts/WebForms/DetailsView.js" />
            <asp:ScriptReference Name="TreeView.js" Assembly="System.Web" Path="~/Scripts/WebForms/TreeView.js" />
            <asp:ScriptReference Name="WebParts.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebParts.js" />
            <asp:ScriptReference Name="Focus.js" Assembly="System.Web" Path="~/Scripts/WebForms/Focus.js" />
            <asp:ScriptReference Name="WebFormsBundle" />
        </Scripts>
    </asp:ScriptManager>

Run the application and our little example will work again as expected

Sadly these new settings are the equivalent to the settings added by code and we need to add them to be able to use the traditional Microsoft AJAX controls.

There's one last thing we need to configure, this is because there's actually a bug with the ValidationSummary control.

To test it, update the Default.aspx page as follows:


    <asp:ValidationSummary ID="ValidationSummary1" runat="server" />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <br />
    <asp:TextBox runat="server" ID="txt" />
    <asp:RequiredFieldValidator ErrorMessage="txt is required" ControlToValidate="txt" runat="server" Text="*" Display="Dynamic" />
    <asp:Button Text="Send info" runat="server" />

Now run the page again, scroll down until you can see the button and click it... woot your page jumped to the top... the workaround to solve this little bug is to add the following code to the Site.master


        <script>
            window.scrollTo = function () {

            };
        </script>

Reference links:



If you found this post useful please leave your comments, I will be happy to hear from you.

12 comments:

  1. Hi Jupaol,
    Thanks for explaining the solution in so much detail. You just saved me a lot of trouble. :)

    Thanks a lot !!

    ReplyDelete
  2. thanks jupaol for the detailed nd ssystematic explanation

    ReplyDelete
  3. This is very helpful and easy to follow

    ReplyDelete
  4. Thanks very much, great example of how to write guides! :)

    ReplyDelete
  5. Hi,

    I want another approach. Since I have attached jquery source to my page, I don't want asp.net reattach it again!

    But it keeps doing that even when I comment BundleConfig.RegisterBundles(BundleTable.Bundles);

    in application_start.

    Any Idea?

    ReplyDelete
  6. Why do you set in Web.config and on each page to WinForms but in Global.asax to None?

    ReplyDelete
  7. Thank you so much - installing the files - really helped

    ReplyDelete
  8. The Keshri Software Solutions provides Web Application development,Website Promotions, Search Engine Optimizations services .we

    have a very dedicated and hard working team of web application developers(asp.net/c#/sql server/MVC) , Search engine optimizers.

    Fast communication and quality delivery product is our commitment.

    To get more details please log on to - http://www.ksoftware.co.in .

    ReplyDelete

If you found this post useful please leave your comments, I will be happy to hear from you.