Sunday, September 23, 2012

Custom ASP.NET SiteMapProvider combining a database, the file system and static content

Hi, I had some trouble while creating a custom SiteMapProvider, at first and after reading several blogs, it appeared that this wasn’t so easy.... but it turns out that it’s really simple once you know the basis.


The SiteMapProvider I want to create, is one that combines data from a database, files in the file system and static nodes defined in the XML sitemap file.


Note: This is not about the Sitemap.xml file used by search engine crawlers, the site map I’m going to build is the sitemap provider used in ASP.Net to ptovide navigation.


Let’s begin.


First thing first. In ASP.Net there’s a default sitemap provider already registered in the web.config file located in: %windir%\Microsoft.NET\Framework\%version%\Config\web.config


And looks like:


    <siteMap>
        <providers>
            <add siteMapFile="web.sitemap" name="AspNetXmlSiteMapProvider"
                type="System.Web.XmlSiteMapProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
        </providers>
    </siteMap>

The XmlSiteMapProvider inherits from: StaticSiteMapProvider


Most of the blogs will tell you that you must inherit StaticSiteMapProvider and implement your own provider almost from scratch. But it turns out that you can inherit XmlSiteMapProvider and customize it. I have not found a problem so far, if you know a problem with this approach please let me know.


So lets create our solution. (I am working with Visual Studio 2012 Express for Web V.11.0.50727.1 RTMREL)


Adding static content


  • Create an empty Web Application called MixedSiteMapProvider in Visual Studio
  • Create a class right under the project root called: CustomSitemapProvider and inherit from XmlSiteMapProvider
  • Register the custom sitemap provider in the web.config file:

        <system.web>
          <siteMap defaultProvider="CustomProvider">
            <providers>
              <add name="CustomProvider" type="MixedSiteMapProvider.CustomSitemapProvider" siteMapFile="Web.sitemap" />
            </providers>
          </siteMap>
        </system.web>

  • Create a sitemap file called Web.sitemap right under the root project and add the following content:

        <?xml version="1.0" encoding="utf-8" ?>
        <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
            <siteMapNode url="~/" title="Home"  description="Static file">
              <siteMapNode url="~/Default2.aspx" title="Default2" description="Another static file">
              </siteMapNode>
              <siteMapNode title="My File System Content" url="" description="">
              </siteMapNode>
            </siteMapNode>
        </siteMap>

  • Create a MasterPage called Site.master right under the project root and add the following code:

        <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MixedSiteMapProvider.Site" %>

        <!DOCTYPE html>

        <html xmlns="http://www.w3.org/1999/xhtml">
        <head runat="server">
            <title></title>
            <asp:ContentPlaceHolder ID="head" runat="server">
            </asp:ContentPlaceHolder>
        </head>
        <body>
            <form id="form1" runat="server">
                <asp:SiteMapDataSource runat="server" ID="smds" ShowStartingNode="true" />
                <div>
                    <asp:SiteMapPath runat="server" ID="siteMapPath1">
                    </asp:SiteMapPath>
                </div>
                <div>
                    <asp:Menu runat="server" ID="menu" DataSourceID="smds">
                    </asp:Menu>
                </div>
                <div>
                    <asp:TreeView runat="server" ID="tv" DataSourceID="smds">
                    </asp:TreeView>
                </div>
                <div>
                    <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
                    </asp:ContentPlaceHolder>
                </div>
            </form>
        </body>
        </html>

  • Create a content ASPX file called Default.aspx right under the project root and select Site.master as its MasterPage

  • Create a content ASPX file called Default2.aspx right under the project root and select Site.master as its MasterPage

  • Run the application and you will see the sitemap provider works as expected reading the content of the Web.sitemap file


Adding File System content


Now we are going to discover the files under a custom path to add them as part of our SiteMapProvider


The StaticSiteMapProvider and the XmlSiteMapProvider works similar. There’s a method called BuildSiteMap in charge to build the nodes structure. The tricky part is to know that this method is actually going to be called on many internal operations of the base class. For example when adding a child node (AddNode method), when finding a node (FindSiteMapNode and FindSiteMapNodeFromKey method), etc.


This is important to keep it in mind because this means that if you do not write taking this consideration you could get a StackOverFlowException. The easiest way to solve this, is creating a flag at instance level (more about this in a minute)


ASP.Net keeps in memory each sitemap provider instance as long as the application is not restarted. This means that by default we get cached our custom sitemap provider. Since we already saw that the BuildSiteMap method is going to be called several times on each page for every concurrent user, we need to build our implementation to be thread-safe. The easiest way to do this is by locking each action with lock


OK, lets roll and create our custom sitemap nodes from the file system.


  • Create a subfolder called Topics right under the project root and add three ASPX files, name them Page1.aspx, Page2.aspx and Page3.aspx

  • Update our CustomSitemapProvider as follows:


using System;
using System.IO;
using System.Linq;
using System.Web;

namespace MixedSiteMapProvider
{
    public class CustomSitemapProvider : XmlSiteMapProvider
    {
        private const string FileSystemContentNodeTitle = "My File System Content";
        private readonly object LockObject = new Object();
        private SiteMapNode WorkingNode { get; set; }
        private bool BuildingNodes { get; set; }

        // this method has to be overriden in order to create the sitemap nodes
        public override SiteMapNode BuildSiteMap()
        {
            // we block the method to make it thread-safe
            lock (LockObject)
            {
                // this condition is the KEY, we need to ensure that this method is executed
                // only once. The problem is that internally, the SiteMapProvider class calls
                // this method several times. If we do not use this condition, we would get a
                // StackOverflowException
                if (this.BuildingNodes)
                {
                    return this.WorkingNode;
                }

                // we call the base BuildSiteMap method to get all the static nodes registered
                // statically in the Web.sitemap file. From here, we will configure this SiteMapNode
                // collection to add our custom nodes
                this.WorkingNode = base.BuildSiteMap();
                this.BuildingNodes = true;

                var fileSystemNode = 
                    this.WorkingNode.ChildNodes.OfType<SiteMapNode>().FirstOrDefault(x => x.Title.Equals(FileSystemContentNodeTitle, StringComparison.InvariantCultureIgnoreCase));

                if (fileSystemNode == null)
                {
                    // if we didn't find a node to explicitly add our content from the file system
                    // we will create a custom node
                    fileSystemNode = new SiteMapNode(this, "FileSystemNode", string.Empty, FileSystemContentNodeTitle);
                    this.AddNode(fileSystemNode, this.WorkingNode);
                }

                // we iterate through all the files contained in the filesystem folder
                foreach (var file in Directory.GetFiles(HttpContext.Current.Server.MapPath("~/Topics/"), "*.aspx"))
                {
                    this.AddNode(
                        new SiteMapNode(this, file, VirtualPathUtility.ToAbsolute("~/Topics/") + Path.GetFileName(file), Path.GetFileNameWithoutExtension(file)), 
                        fileSystemNode);
                }

                return this.WorkingNode;
            }
        }
    }
}

I think the code is self explanatory (along the comments).


I just want to emphasize the importance of the lock process lock (LockObject) to make the method thread-safe and the flag condition if (this.BuildingNodes) to prevent the execution of the method more than once.


If you run your application you will see something like:






Adding database records


The last step is to complement our dynamic sitemap with records from the database.


Since we already created the most difficult part, this should be really simple.


I’m going to use the PUBS database, and let say that we want to show in our navigation all the job descriptions that exist in the jobs table and inside each one of these categories we want to list all its employees.


  • Get the PUBS database from here and install it

  • Add the following connection string to your web.config file


  <connectionStrings>
    <add name="Pubs" providerName="System.Data.SqlClient" connectionString="Data Source=.\sqlexpress;Initial Catalog=pubs;Integrated Security=True" />
  </connectionStrings>


  • Create a new LINQ To SQL class named Pubs right under the root of your project

  • Connect to the database and drag and drop the jobs and employee tables to de PubsDataContext designer. Save the file and close it

  • Add three content ASPX files choosing the Site.master master page right under the root of your project and name them: JobsList.aspx, JobDetails.aspx and EmployeeDetails.aspx

  • Add the following markup to the JobsList.aspx file



<%@ Page Title="" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="JobsList.aspx.cs" Inherits="MixedSiteMapProvider.JobsList" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <asp:LinqDataSource runat="server" ID="lds"
        TableName="jobs" ContextTypeName="MixedSiteMapProvider.PubsDataContext">
    </asp:LinqDataSource>
    <asp:GridView runat="server" ID="gv" DataSourceID="lds" AutoGenerateColumns="false">
        <Columns>
            <asp:HyperLinkField HeaderText="Job description" DataTextField="job_desc" DataNavigateUrlFormatString="~/JobDetails.aspx?id={0}" DataNavigateUrlFields="job_id" />
        </Columns>
    </asp:GridView>
</asp:Content>


  • Add the following markup to JobDetails.aspx


    <asp:LinqDataSource runat="server" ID="lds"
        TableName="jobs"
        ContextTypeName="MixedSiteMapProvider.PubsDataContext">
    </asp:LinqDataSource>
    <asp:QueryExtender runat="server" ID="qeJob" TargetControlID="lds">
        <asp:PropertyExpression>
            <asp:QueryStringParameter Name="job_id" Type="Int16" ValidateInput="true" QueryStringField="id" />
        </asp:PropertyExpression>
    </asp:QueryExtender>
    <asp:DetailsView runat="server" DataSourceID="lds" />
    <hr />
    <asp:LinqDataSource runat="server" ID="ldsempl"
        TableName="employee"
        ContextTypeName="MixedSiteMapProvider.PubsDataContext">
    </asp:LinqDataSource>
    <asp:QueryExtender runat="server" ID="qeEmpl" TargetControlID="ldsempl">
        <asp:PropertyExpression>
            <asp:QueryStringParameter QueryStringField="id" Type="Int16" ValidateInput="true" Name="job_id" />
        </asp:PropertyExpression>
    </asp:QueryExtender>
    <asp:GridView runat="server" ID="gv" DataSourceID="ldsempl" AutoGenerateColumns="false" ItemType="MixedSiteMapProvider.employee">
        <Columns>
            <asp:TemplateField HeaderText="Employee name">
                <ItemTemplate>
                    <asp:HyperLink NavigateUrl='<%# "~/EmployeeDetails.aspx?id=" + Item.emp_id %>' runat="server" Text='<%# Item.fname + " " + Item.lname %>' />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
    </asp:GridView>


  • Add the following code to EmployeeDetails.aspx


    <asp:LinqDataSource runat="server" ID="lds"
        TableName="employee" ContextTypeName="MixedSiteMapProvider.PubsDataContext">
    </asp:LinqDataSource>
    <asp:QueryExtender runat="server" ID="qeEmployee" TargetControlID="lds">
        <asp:PropertyExpression>
            <asp:QueryStringParameter Name="emp_id" Type="String" ValidateInput="true" QueryStringField="id" />
        </asp:PropertyExpression>
    </asp:QueryExtender>
    <asp:DetailsView runat="server" DataSourceID="lds" />


  • Add the following node to the Web.sitemap file


      <siteMapNode title="PUBS Jobs">
      </siteMapNode>


  • Add the following code to the CustomSitemapProvider class


private const string PubsContentNodeTitle = "PUBS Jobs";

  • Add the following code to the end of the BuildSiteMap method in the CustomSitemapProvider class, just before the return this.WorkingNode; line


        // adding the jobs and employees from the database to the sitemap
        var pubsNode = this.WorkingNode.ChildNodes.OfType<SiteMapNode>().FirstOrDefault(x => x.Title.Equals(PubsContentNodeTitle, StringComparison.InvariantCultureIgnoreCase));

        // if the node does not exists, we will create a new node to serve as the base
        // for our database nodes
        if (pubsNode == null)
        {
            pubsNode = new SiteMapNode(this, PubsContentNodeTitle, VirtualPathUtility.ToAbsolute("~/JobsList.aspx"), PubsContentNodeTitle);
            this.AddNode(pubsNode, this.WorkingNode);
        }

        using (var ctx = new PubsDataContext())
        {
            foreach (var empl in ctx.employee)
            {
                var job = empl.jobs;
                var jobNode = this.FindSiteMapNodeFromKey(string.Format("Job:{0}", job.job_desc));

                // if the job node has not been created yet, we will create it
                if (jobNode == null)
                {
                    jobNode = new SiteMapNode(this, string.Format("Job:{0}", job.job_desc), VirtualPathUtility.ToAbsolute("~/JobDetails.aspx?id=" + job.job_id.ToString()), job.job_desc);
                    this.AddNode(jobNode, pubsNode);
                }

                // we add the employee node
                this.AddNode(
                    new SiteMapNode(this, "Employee:" + empl.emp_id, VirtualPathUtility.ToAbsolute("~/EmployeeDetails.aspx?id=" + empl.emp_id), empl.fname + " " + empl.lname), 
                    jobNode);
            }
        }


That’s it.... wow this was a really loong post... sorry about that. There was a lot of code for a simple task =(


If you run the application you will something like this: (after applying some style)




Download the code of this article

Browse the full code in Github

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.