Monday, May 12, 2008

Loading and Executing JavaScript Files From JavaScript, ASP.NET AJAX

Download source code with samples for the article here.

Quite often we need to load js files dynamically right from javascript. With ASP.NET AJAX it is simple.

ASP.NET AJAX library has internal ScriptLoader class that can be used to load js files, specify callbacks that will be invoked when script is downloaded and ready, and execute functions inside newly loaded files.

I will not list all available methods from ScriptLoader class - you can see them in more details in AJAX source.

I will demonstrate how to load files and execute some functions in it.

ScriptLoader has static and instance members. To add script references(urls) and script blocks to the loader we have to create instance of the class:

image

To add one or more script references to the loader use queueScriptReference method:

image

the parameter is path to the script file.

And finally to load the file and insert link element to the body of the document use the following function:

image

 

Actually the loadScripts expects more parameters, but only first is required. The declaration of loadScripts function in AJAX library looks like this:

image

As you see the function can accept three callback functions that will be called when some condition is true: load was successful, load failed, or load was timed out.

 

Whole sample will look like:

image

Test.js file contains the following code:

image

First line simply executes alert function.

Second line is very important - this is how ScriptLoader knows when script is loaded to the end and calls success callBack.

If you don't add this line to the end of the file ScriptLoader will never know is or is not script executed (loaded) to the end.

Lets run the page:

 image

Script was loaded and executed!

 

Now lets try another example, where we will pass callback to the function to be able to call function inside dynamically loaded file, and be sure that it is loaded before we call anything in it.

The page will look like:

image

and js file:

image

As you see we are calling the function inside file in callback, this way we ensure it is called AFTER file was loaded.

Lets execute the page and first try to click second button:

image

Alert button says the script is not loaded yet - that's true. Now after we load the script (clicking on first button):

image

Is works.

 

You may be wondering why you need this. But suppose you are loading contents to the browser dynamically, for example using web service or page method call, and the content is using javascript. You can also load dynamically it's script file and execute anything you need there.

 

Hope this helps.

 

Technorati Tags:

 

kick it on DotNetKicks.com

Using AjaxRepeater Control, ASP.NET AJAX

You can download full source code with samples and controls for the article here.

Recently after reading blog post Ajax Templates by Nikhil Kothari I really liked the idea, the source code is great, and the control is huge help for AJAX development.

In this blog post I want to share what I learned after investigating sample more carefully, and in what scenarios I feel this control is doing it's job quite well.

I want also to clarify that I will discuss this control's client side API, not touching it's server side capabilities.

Also one thing to mention - I converted the source code from the sample to stand alone server control, and changed it's namespace to one used in my library. Please read carefully the header of the source for the control, it states the license and says you can freely use the source for commercial or not commercial software, there you can also find some limitations.

Also - I modified only few lines of original code in the override of DataBind method, I wanted the template to be constructed even if I have no server side DataSource property specified.

So what is AjaxRepeater control, and how to use it?

To create Template, just drag the control on the page and specify template inside it, for example this way:

image

Here "itemTemplate" is the id of the element inside repeater. And inner XML/HTML of this element is the template representing one data item. In this case the template that will be repeated many times inside single "UL" element is:

image

"itemTemplate" id should be used for every template container, even if you have multiply AjaxRepeater controls on the page. Repeater finds template container to add template items to it by this id, searching for it inside parent AjaxRepeater control, which in the browser is rendered to DIV or SPAN element, depending on server-side property "RenderMode" which can be set to "Inline" or "Block". So this markup:

image

will be rendered in the browser (with empty data source) this way:

image

and this markup:

image

will be rendered as:

image

 

On the client side the repeater is javascript object, deriving from Sys.UI.Control, and it can be accessed using $find shortcut, specifying the ClientID of the control as control's ID. For example we can use this code to access the object:

image

The most important functions that we can call on the object are:

set_data(value) - where value is the array of objects that is used to generate as many html items as are data items in the array. After calling this function, whole inner contents of item container will be cleaned and new items generated and added to the document under container element.

addDataItem(dataItem) - adds one item to the container under last one. dataItem here is the object that will be used to generate one item from template.

So we can use the following tricks:

to clear the container:

image

to bind repeater to data source:

image

to incrementally add items to the container:

image

You may be interested what is generated in the html markup, let's see the result of execution of last sample:

image

And visually it looks like this:

image

Ok, lets see the simplest sample page that shows how to create items dynamically:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Sample1.aspx.cs" Inherits="Sample1" %>
<%@ Register Assembly="Devarchive.Net" Namespace="Devarchive.Net" 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 id="Head1" runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="SM">
    </asp:ScriptManager>

    <script type="text/javascript">
        function addItem(value) {
            var data = {text:value};
            $find("AjaxRepeater1").addDataItem(data);
        }
    </script>

    <div>
        <cc1:AjaxRepeater ID="AjaxRepeater1" runat="server" RenderMode="Block">
            <ul id="itemContainer">
                <li>{text}</li>
            </ul>
        </cc1:AjaxRepeater>
        <input type="text" id="txt" />
        <input type="button" value="Add List Item" onclick="addItem($get('txt').value);" />
    </div>
    </form>
</body>
</html>

 

As you see the code is really simple - when you enter some text in the text box and then click "Add List Item" button, new Item is generated and added to the container.

The screen shot shows what we get:

image

 

Ok, lets look into templates, and data that is used to built the template. It is really simple. In the samples above

Template:

image

is used to build item:

image

using this dataItem:

image

To build table rows for example,

Repeater markup:

image

can be used to build the table with rows:

image

using this code:

image

 

Ok, but who said we cannot use repeater to generate only one item, and this way use it as client side view rather then repeater?

This is simple, suppose we have the following view:

image

Here we can use the following code to rebind view with new data:

image

 

I want to say one more thing - js script for templating feature uses innerHTML to initially retrieve and parse the template in the browser, and this makes problems when we want to use templates inside style attributes of the elements.

for example when using the following template, script cleans out the style attribute inner contents, when accessing it using innerHTML:

image

And renders it without styles, so to fix this I used a little fix/amend to the original script, With this approach you should use "_style" attribute instead of "style". Then when templating script loads the template it just strips out the leading underscore and generates correct items.

 

Now ! I want to show you the scenario which is really easy to achieve with this control !

Suppose you want to bind data from the web service. For example you want to fetch data and bind it to the view or repeater. It is simple!

As we know web services (and page methods) have nice built in feature - when called from javascript they are serializing return objects into JSON - and therefore can be used to fetch data and set it to the repeater right from the browser.

The source code for the sample page demonstrating this is:

 

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!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 id="Head1" runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="SM">
        <Services>
            <asp:ServiceReference Path="~/WebService.asmx" />
        </Services>
    </asp:ScriptManager>
    <script type="text/javascript">
        function bind() {
            WebService.GetData(function(result) {
                var rpt1 = $find("rpt1");
                rpt1.set_data(result);
            });            
        }
    </script>
    <div>
        <input type="button" onclick="bind();" value="Bind From Service" />
        <dn:AjaxRepeater runat="server" ID="rpt1">
        <table cellpadding="4" cellspacing="2" width="400px">
            <tbody id="itemContainer">
                <tr>
                    <td _style="{style1}">{field1}</td>
                    <td _style="{style2}">{field2}</td>
                    <td _style="{style3}">{field3}</td>
                    <td _style="{style4}">{field4}</td>
                </tr>
            </tbody>
        </table>
        </dn:AjaxRepeater>
    </div>
    </form>
</body>
</html>

 

WebService.cs :

 

using System;
using System.Collections;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Collections.Generic;
using System.Web.Script.Services;

[WebService]
[ScriptService]
public class WebService : System.Web.Services.WebService
{
    Random r = new Random();

    [WebMethod]
    [ScriptMethod]
    public List<Data> GetData()
    {
        List<Data> data = new List<Data>();
        for (int i = 0; i < 5; i++)
        {
            Data d = new Data();
            d.field1 = "field1_" + i.ToString();
            d.field2 = "field2_" + i.ToString();
            d.field3 = "field3_" + i.ToString();
            d.field4 = "field4_" + i.ToString();
            d.style1 = "background-color:" + getRandomColor();
            d.style2 = "background-color:" + getRandomColor();
            d.style3 = "background-color:" + getRandomColor();
            d.style4 = "background-color:" + getRandomColor();
            data.Add(d);
        }
        return data;
    }

    private string getRandomColor()
    {
        string result = "#";
        for (int i = 0; i < 6; i++)
        {
            result += r.Next(0, 10).ToString();
        }
        return result;
    }

    public class Data
    {
        public string field1;
        public string field2;
        public string field3;
        public string field4;
        public string style1;
        public string style2;
        public string style3;
        public string style4;
    }
}
Is not this simple?
Here is the result page's screen shot:
 

image

 

Guys, we can use the control to build really cool dynamic contents using templates and web services !

 

Ok, but what about performance ? -

As I measured - on my computer (Athlon 64 4800+, 2 GB RAM DDR2) to fetch and build 5000 items in the repeater with markup used above it took only 13 seconds, this means 13/5000 = 0.0026 seconds for one template. Taking into account that performance degrade gracefully with growth of html markup size, these are really good figures. Besides this some time is spent to generate response on the server, and deserialize it on the client, all this is done in 13 seconds.

 

Now suppose scenarios when we have a lot of repeaters and views on the page - only templates are loaded first time, and all the data is bound later. This is really good approach very often - we can bind whole grids with sorting/paging this way, just need to specify sort direction and page number when calling web service. Or we can use master-detail relationship - where we select row in the table (master) which was bound using service, and in click event of the row bind view with corresponding data from service (details).

 

I hope this article will help you start with the control, And want to thank Nikhil for the great work.

 

Technorati Tags:

 

kick it on DotNetKicks.com

Saturday, May 3, 2008

Cross Browser GridView Fixed Header Extender, ASP.NET AJAX

I have created ASP.NET AJAX Extender control. It extends GridView control fixing it's header on the top while adding vertical scroll bar to it's contents.

The control also supports maintaining scroll position inside grid between postbacks.

The control works fine inside or outside of UpdatePanel.

 

To extend GridView, just drop extender onto the design surface and set its TargetControlID:

image

Width and height of scrollable area should be specified on GridView control itself:

image

And here is the result:

image

Now let's assign width 100% to the grid:

image

let's resize browser window:

image

As you see header cells are always above corresponding column.

 

I will place code for the control explaining main operations it does.

The steps it does are the following:

1) when extender is being initialized in the browser, Table element of the GridView is "shallow cloned" using clone javascript function, the cloning is done with it all attributes including style. The clone is placed before actual GridView in the document tree.

2) Header of original GridView is cloned and placed to the cloned table (see above)

3) Create a div element which will allow us scroll the contents of a table and place it inside new row in the cloned table

4) copy remaining rows from original GridView inside div element, also hide original GridView's header and remove height and width style elements from table element we are copying.

5) call function that iterates over all first row's cells in the contents table and sets corresponding widths to the header cells.

6) subscribe to "resize" event of window object, and in the handler do the same widths check for header

7) subscribe to "scroll" event of div element, every time div is scrolled store scroll position in hidden field registered by extender

8) if we already have scroll position stored - just set it to the div position (this step is important after postback - when we need to restore scroll position.

 

Ok here is the code for GridViewFixedHeaderExtender.cs file:

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

namespace Devarchive.Net
{
    [
        AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal),
        AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal),
        Designer("Devarchive.Net.SimpleDesigner, Devarchive.Net"),
        ToolboxData("<{0}:GridViewFixedHeaderExtender runat=server></{0}:GridViewFixedHeaderExtender>"),
        TargetControlType(typeof(GridView))
    ]
    public class GridViewFixedHeaderExtender : ExtenderControl
    {

        #region Overrides
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
        }

        protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
        {
            if (TargetControl == null || !TargetControl.Visible || TargetControl.Rows.Count == 0)
            {
                TargetControl.Height = Unit.Empty;
                yield break;
            }
            ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("Devarchive.Net.GridViewFixedHeaderExtender", targetControl.ClientID);
            descriptor.AddProperty("scrollField", HiddenFieldID);
            yield return descriptor;
        }

        protected override IEnumerable<ScriptReference> GetScriptReferences()
        {
            if (TargetControl == null || !TargetControl.Visible || TargetControl.Rows.Count == 0)
            {
                TargetControl.Height = Unit.Empty;
                yield break;
            }
            yield return new ScriptReference("Devarchive.Net.GridViewFixedHeaderExtender.js", this.GetType().Assembly.FullName);
        }

        protected override void Render(HtmlTextWriter writer)
        {
            ScriptManager.RegisterHiddenField(
                this,
                HiddenFieldID,
                LastScroll.ToString()
                );
            base.Render(writer);
        }

        private GridView TargetControl
        {
            get
            {
                GridView result = this.NamingContainer.FindControl(TargetControlID) as GridView;
                return result;
            }
        }

        private int LastScroll
        {
            get
            {
                int result = 0;
                if (Page.Request[HiddenFieldID] != null)
                {
                    int.TryParse(Page.Request[HiddenFieldID], out result);
                }
                return result;
            }
        }

        private string HiddenFieldID
        {
            get
            {
                return String.Format("{0}_GVFHE_Scroll", ClientID);
            }
        }

        #endregion
    }
}

-----------------------------

And here is js file listing:

-----------------------------

 

/// <reference name="MicrosoftAjax.debug.js" />
/// <reference name="MicrosoftAjaxTimer.debug.js" />
/// <reference name="MicrosoftAjaxWebForms.debug.js" />

Type.registerNamespace("Devarchive.Net");

Devarchive.Net.GridViewFixedHeaderExtender = function(element) {
    Devarchive.Net.GridViewFixedHeaderExtender.initializeBase(this, [element]);
    this._documentResizeDelegate = null;
    this._lock = false;
    this._mainTableID = null;
    this._innerTableID = null;
    this._divChild = null;
    
    this._scrollField = 0;
}

Devarchive.Net.GridViewFixedHeaderExtender.prototype = {
    // Overrides
    //#region
    initialize: function() {
        Devarchive.Net.GridViewFixedHeaderExtender.callBaseMethod(this, 'initialize');
        this.initGrid();
    },
    dispose: function() {        
        //Add custom dispose actions here
        $removeHandler(window, "resize", this._documentResizeDelegate);
        if(this._divChild) {
            $clearHandlers(this._divChild);
        }
        Devarchive.Net.GridViewFixedHeaderExtender.callBaseMethod(this, 'dispose');
    },
    //#endregion
    
    // Properties
    //#region
    get_scrollField : function() {
        return this._scrollField;
    },
    
    set_scrollField : function(value) {
        if (this._scrollField !== value) {
            this._scrollField = value;
            this.raisePropertyChanged('scrollField');
        }
    },
    //#endregion
    
    // Methods
    //#region
    getLastScroll : function() {
        var result = 0;
        var hf = $get(this._scrollField);
        if(hf) {
            result = parseInt(hf.value);
            if(!result) result = 0;
        }
        return result;
    },
    setLastScroll : function(value) {
        var hf = $get(this._scrollField);
        if(hf) {
            hf.value = value;
        }
    },
    initGrid : function() {
        // create deep clone of target grid
        var target = this.get_element();
        var clone = target.cloneNode(true);
        
        // get desired height of inner scrollable area
        var height = target.style.height;
        var width = target.style.width;
        
        var mainTable = target.cloneNode(false);
        mainTable.id = String.format("outer_{0}", target.id);
        target.parentNode.insertBefore(mainTable, target);


        var mainHead = document.createElement("thead");
        mainTable.appendChild(mainHead);
        var mainBody = document.createElement("tbody");
        mainTable.appendChild(mainBody);
        
        // Clone original header
        var header = target.rows[0].cloneNode(true);
        mainHead.appendChild(header);

        // add scrollable area mainTable
        var secondRow = document.createElement("tr");
        mainBody.appendChild(secondRow);
        var mainTd = document.createElement("td");
        secondRow.appendChild(mainTd)
        this.setAttribute(mainTd, "colspan", target.rows[0].cells.length);
        this.setAttribute(mainTd, "align", "left");
        this.setAttribute(mainTd, "valign", "top");
        var divChild = document.createElement("div");
        mainTd.appendChild(divChild);
        divChild.style.width = width;
        divChild.style.height = height;
        $addHandler(divChild, "scroll", Function.createDelegate(this, this.syncScroll));
        divChild.style.overflow = "auto";
        divChild.style.overflowX = "hidden";
        divChild.style.overflowY = "scroll";
        this._divChild = divChild;
        
        // now remove old grid from document and insert new clone into the place
        target.parentNode.removeChild(target);
        divChild.appendChild(clone);
        
        // assign extender related data to clone
        clone._behaviors = target._behaviors;
        clone.GridViewFixedHeaderExtender = target.GridViewFixedHeaderExtender;
        
        // correct styles
        var attributes = [];
        for(var i = 0; i < clone.attributes.length; i++) {
            var attr = clone.attributes.item(i);
            var value = attr.value.trim().toLowerCase();
            if(value != "cellpadding" && value != "cellspacing") {
                Array.add(attributes, attr);
            }
        }
        Array.forEach(attributes, this.deleteAttribute, clone);
        clone.deleteRow(clone.rows[0]);
        clone.border = "0";
        clone.style.borderWidth = "0px";
        clone.style.width = "100%";
        clone.style.height = "";
        mainTable.style.height = "";
        target.style.height = "";
        
        // correct widths of header columns and subscribe to document resize event:
        this._mainTableID = mainTable.id;
        this._innerTableID = clone.id;
        this._documentResizeDelegate = Function.createDelegate(
            this,
            this.syncWidths
        );
        this._documentResizeDelegate.call();
        // Attach to window's resize event to resize header cells when inner cells change their size
        $addHandler(window, "resize", this._documentResizeDelegate);  
        // Restore scroll position from last time  
        divChild.scrollTop = this.getLastScroll();
    },
    
    setAttribute : function(element, attribute, value) {
        var namedItem = document.createAttribute(attribute);
        namedItem.value = value;
        element.attributes.setNamedItem(namedItem);
    },
    deleteAttribute : function(attribute, index, attributes) {
        this.removeAttribute(attribute);
    },
    syncScroll : function(args) {
        if(this._divChild) {
            this.setLastScroll(this._divChild.scrollTop);
        }
    },
    syncWidths : function(args) {
        if(!this._lock) {
            this._lock = true;
            var mainTable = $get(this._mainTableID);
            var innerCellPadding = mainTable.cellPadding;
            var header = mainTable.rows[0];
            var innerTable = $get(this._innerTableID);
            var originalRow = innerTable.rows[0];
            var headerWidth = Sys.UI.DomElement.getBounds(header).width;
            var originalRowWidth = Sys.UI.DomElement.getBounds(originalRow).width;
            var diff = headerWidth - originalRowWidth - innerCellPadding * 2;
            if (originalRow && header) {
                for(var i = 0; i < originalRow.cells.length; i++) {
                    var bounds = Sys.UI.DomElement.getBounds(originalRow.cells[i]);
                    var x = bounds.width;
                    if(i == originalRow.cells.length-1) {
                        x = x + diff - innerCellPadding * 2;
                    } else {
                        x = x - innerCellPadding;
                    }
                    header.cells[i].style.width = x + "px";
                }
            }
            this._lock = false;
        }
    }
    //#endregion
}
Devarchive.Net.GridViewFixedHeaderExtender.registerClass('Devarchive.Net.GridViewFixedHeaderExtender', Sys.UI.Behavior);
 
You can download source code for the Devarchive.Net toolkit including this control here.
To the moment toolkit contains four controls:
StateBag control
HoverTooltip control
Timer control
GridViewFixedHeaderExtender control
 

StateBag control in the toolkit is little modified to support scenarios when you might need to use it inside UpdatePanel control.

 

Hope this helps.

Technorati Tags: