CategoryWebForms

WebForms and triggering submit buttons on pressing the enter key

Default button

Filling in forms by only using keyboard commands is very popular and should get more attention than most developers give it.
I have seen a lot of weird JavaScript functions in a WebForms page to fake a button click when a user presses the enter button.
This is really unnecessary. There is a nice feature which allows you to set the submit button for a (sub)section of a WebForms page.
Every container element (f.e. Panel) supports this feature. Pressing enter on any focusable element will trigger a click on the button defined by the DefaultButton property of the parent container element.

<asp:Panel ID="pnlFormName" runat="server" DefaultButton="btnSubmit">
    <asp:TextBox ID="txtName" runat="server" />
    <asp:TextBox ID="txtMiddleName" runat="server" />
    <asp:TextBox ID="txtLastName" runat="server" />
    <asp:Button ID="btnSubmit" runat="server" />
</asp:Panel>

Tab order

Another thing to keep in mind is the tab order. The order can be set by setting the TabIndex property of an HTML Input element (or any other element which should be focusable).
The default tab order will fit most cases; it is equal to the positioning order of the elements.

<asp:Panel ID="pnlFormName" runat="server">
    <asp:TextBox ID="txtName" runat="server" TabIndex="1" />
    <asp:TextBox ID="txtMiddleName" runat="server" TabIndex="2" />
    <asp:TextBox ID="txtLastName" runat="server" TabIndex="3" />
    <asp:Button ID="btnSubmit" runat="server" />
</asp:Panel>

Providing data to JavaScript functions from code behind

In my previous post, I talked about JavaScript namespaces and functions.
When using WebForms, it can be difficult to call these functions from the code behind in a nice way.
It usually requires some data to initialize the JavaScript function, for example, providing some html element ids as trigger elements.
The Ids of html elements are non-predictable (except when using static ids, but this brings in a whole bunch of different problems) and should be provided from the code behind to avoid the ugly <% %> syntax in your markup file.
I see a lot of people making use of a StringBuilder to write out a JavaScript object. This will work of course, but it is not the nicest and best way to do that, because you will lose your strongly typing and intellisense.

I prefer to create a model and use a JavaScript Serializer to create a json object and provide that to a function.


public class SearchManagerOptions
{
    public string Url { get; set; }
}

.Net has its own serializer; JavascriptSerializer. It is a very simple and straight forward serializer and does the trick.


var options = new SearchManagerOptions { Url = "someurlhere" };
var json = new JavaScriptSerializer().Serialize(options);

The result (json) will look like


"{\"Url\":\"SomeUrl\"}"

This is fine for most common cases.

If you want some extra control over the serialization process, the JavaScriptSerializer will not be your friend and I recommend you to switch to the serializer of json.NET. This is a third party library (available on NuGet) which allows you to control the serialization process by decorating your properties with attributes.
For example, when I want to apply to the JavaScript standard of camel casing properties, I can use an attribute to change the output name of the property.


public class SearchManagerOptions
{
    [JsonProperty("url")]
    public string Ur; { get; set }
}

The json.NET seializer is used as follows:


var options = new Models.SearchManagerOptions { Url = "SomeUrl" };
var json = JsonConvert.SerializeObject(options);

This results in:


"{\"url\":\"SomeUrl\"}"

That’s even better, we now apply to the standard!
Just play around with the json.Net library, it is full of nice serialization tricks. For example, ignoring properties for serialization by adding the JsonIgnore attribute to properties.

Now we need to call the JavaScript function and provide this data.
WebForms provides us two methods for injecting scripts; RegisterClientScriptBlock and RegisterStartupScript.
What is the difference? Good question! Both method signatures are the same. The difference lies in the position where the script is injected in the page.
RegisterClientScriptBlock injects the script at the top of the form element (as one of the first children). This means that none of the html elements are rendered yet. Remember that! All your selectors won’t work! Unless you use a document ready event.
RegisterStartupScript injects the script at the bottom of the form element, which means that all html elements are already rendered.
I usually go for RegisterStartupScript, because I think it is a cleaner solution to inject scripts at the end of your page. It is still injected inside the form element, but that is a limitation of WebForms.


var options = new Models.SearchManagerOptions { Url = "SomeUrl" };
var json = JsonConvert.SerializeObject(options);
var script = string.Format("ViCreative.Managers.SearchManager.init({0})", json);

if (Page.ClientScript.IsStartupScriptRegistered("SearchManagerInitialization"))
{
    Page.ClientScript.RegisterStartupScript(GetType(), "SearchManagerInitialization", script, true);
}

I do not want this script to be injected more than once, which is why I check if this script is already registered. I usually would add “SearchManagerInitialization” to a constant, but for clarity of this blog post, I just added it to the script.

Automatic Bundling and Minification in .Net

One of the cool new features of .Net 4.5 is the bundling and minification feature. This can save so many requests to your server and saves you many kbs without creating a debug hell. The amount of data is getting more and more important, because of the mobile devices. People pay lots of money on data bundles for their mobile device, we better not waste it. It also decreases loading time!! Reasons enough to start using bundling and minification!

First thing you need to do is download and install the Microsoft ASP.NET Web  Optimization Framework nuget package.

The bundles are registered at application startup. This event is handled in the Application_Start method in the Global.asax. I always try to keep my Global.asax as clean as possible and split all registrations to seperate files in the App_Start folder. So I created a static BundleConfig class in the App_Start folder. This class contains one method, Register with the current Bundles as argument.

BundleConfig.RegisterBundles(BundleTable.Bundles);

BundleConfig

The BundleConfig class registers all JavaScript and Stylesheet bundles, Both Bundles can be added to the same BundleCollection, but both bundles have their own classes. JavaScript bundles are created by using the ScriptBundle class and Stylesheet Bundles are created by using the StyleBundle class.

bundles.Add(new ScriptBundle("~/bundles/jquery").Include("~/Scripts/jquery-{version}.js"));

This creates a JavaScript bundle named “~/bundles/jquery”  and includes one file, “~/Scripts/jquery-{version}.js”.  {version} is automatically resolved to the version of the jquery file in the scripts folder.

bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

This creates a Stylesheet bundle names “~/content/css”” and contains one file “~/Content/site.css”. Adding more files is easy, just add more files to the include and separate them by a comma.

Adding bundles to a view

The only thing left to do, is adding the bundles to a view.

MVC (Razor)

@Scripts.Render("~/bundles/jquery")

WebForms

<%: Scripts.Render("~/bundles/jquery") %>

The above code renders the JavaScript bundle.

MVC (Razor)

@Styles.Render("~/Content/css")

WebForms

<%: Styles.Render("~/Content/css") %>

The above code renders the Stylesheet bundle.

All the requests to the bundles have a querystring parameter (v=CkVTG71m7lHB5jSCpyOSxbeCVJLIPag7u7NL4ykFenk1). This is to make sure that no old versions of this file are cached and retrieved.

Config files

Bundles can also be configured in config files. This allows you to make changes to your bundle without having to rebuild the project. It also allows frontend developers to easily include, remove or change files in a bundle without having to use visual studio.

An example of a bundle.config file

<?xml version="1.0" encoding="utf-8" ?>
<bundles version="1.0">
  <styleBundle path="~/Content/css">
    <include path="~/Content/Site.css" />
  </styleBundle>
</bundles>

Debugging

One of the biggest problems of minified files is debugging. It’s almost impossible to debug those files.

That’s when the best feature of this minification framework comes in place. When compilation is set to debug (in web.config), the files will not be minified, which makes it a lot easier to debug.

<system.web>
    <compilation debug="true" />
    <!-- Lines removed for clarity. -->
</system.web>

Creating custom bundle transforms

Custom transforms can be created by implementing the IBundleTransform interface.,

using System.Web.Optimization;

public class MyByndleTransform : IBundleTransform
{
   public void Process(BundleContext context, BundleResponse response)
   {
       // Process bundle tranform here
   }
}

The custom transform can now be added to the transform of a bundle

var myBundle = new Bundle("~/My/Files/To/Include");
lessBundle.Transforms.Add(new MyBundleTransform());

This bundle can now be added to the bundles collection in the BundleConfig file.

Why creating custom bundle transforms? Just think of less or coffeescript, or whatever bundle you want.

UpdatePanels and JavaScript triggers

Sometimes, you don’t get to choose between WebForms and MVC (for instance when you’re doing some rework on an old project).
So I got stuck on a WebForms project and I’m more a MVC kinda guy.
Some reasons why I like MVC over WebForms:

  • Flexibility
  • Unit testable
  • Stateless
  • Clean html output

Some people say that you have to write more html with MVC, but I don’t really agree. When you make good use of the HtmlExtensions, DisplayTemplates and EditorTemplates, it is just the same and even more flexible.
But that’s not what this post is about.
When I was working on this WebForms project, I was creating a page with multiple UpdatePanels and that’s when the trouble started.
I wanted to update the UpdatePanel form outside the UpdatePanel, so there was no use in defining triggers. The trigger was coming from a JavaScript function.
After doing some research, I read about the __doPostBack function. This function allows you to update an UpdatePanel and takes two arguments. The first argument is the ClientID of the UpdatePanel:

<asp:UpdatePanel runat="server" ID="upDemo">
  <ContentTemplate>
    <h1>Inside the UpdatePanel</h1>
  </ContentTemplate>
</asp:UpdatePanel>

The following JavaScript code will trigger an update of the UpdatePanel in the demo above:

__doPostBack("<%= upDemo.ClientID %>", "some argument");

The second argument of the __doPostBack function is the event argument. This is a very nice feature, because this allows you to send an ID or some extra data. You can even send a json object if you want.

But now we need to intercept the data in the code behind. The __doPostBack causes a PostBack so all events of the page cycle will occur.  In the page load we can intercept the ajax event.

First of all, there is the ScriptManager. This manager handles the Ajax requests and provides useful data.

protected void Page_Load(object sender, EventArgs e)
{
  var scriptmanager = ScriptManager.GetCurrent(this);
  var isAjaxRequest - scriptmanager.IsInAsyncPostBack;
}

This code example shows how the ScriptManager exposes the IsInAsyncPostBack property. This property helps to determine if the request is an ajax request (Similar to Request.IsAjaxRequest in MVC). But what if you have more than one UpdatePanel? Which panel is causing the ajax request?
I found two ways to determine this, The first one is again the ScriptManager:

protected void Page_Load(object sender, EventArgs e)
{
  var scriptmanager = ScriptManager.GetCurrent(this);
  if (scriptmanager.IsInAsyncPostBack
   && scriptmanager.AsyncPostBackSourceElementID == upDemo.ClientID)
  {
    // we now know that the postback is caused by the upDemo UpdatePanel
  }
}

The AsyncPostBackSourceElementID holds the ClientID of the UpdatePanel which caused the PostBack.
The second way is by using  the Form values from the Request:

protected void Page_Load(object sender, EventArgs e)
{
  var target = this.Request.Form["__EVENTTARGET"];
  var scriptmanager = ScriptManager.GetCurrent(this);
  if (scriptmanager.IsInAsyncPostBack && target == upDemo.ClientID)
  {
    // we now know that the postback is caused by the upDemo UpdatePanel
    var argument = this.Request.Form["__EVENTARGUMENT"];
    // argument is the second argument of the __doPostBack function.
  }
}

The Form key __EVENTTARGET holds the ClientID of the UpdatePanel, similar to the AsyncPostBackSourceElementID

The Form key __EVENTARGUMENT holds the second argument of the __doPostBack function. When the __doPostBack function is called with only one argument, the value in __EVENTARGUMENT can be something like “undefined”. This depends on the browser. I recommend to send an empty string as second argument when no data is required for the request.

Check my follow up post here