Monday, February 25, 2008

Attaching custom client-side data to ASP.NET AJAX WebRequest objects.

Often we need to store some client-side data from request to request. Because the browser supports two simultaneous connections to the server it's easy to do two concurrent requests to the server - and when response is received we want to somehow differentiate between that requests. For example when I click on Button1 - I want to pass let's say exact time when request was invoked, or some javascript object on which I need to perform some actions in endRequestHandler.

So what I want to do - attach some javascript object to the request somehow, to later use it when that request is served by the server and response is sent back. At the same time I don't want to sent that attached object to the server with request and then receive it from the server, because it's not efficient - we can store that object on client side.

To do this I will use userContext Property of WebRequest Class in ASP.NET AJAX library.

I created the sample page. The code looks like:

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

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
    <style type="text/css">
        .loggerDiv
        {
            width: 200px;
            height: 400px;
            overflow: auto;
            float: left;
            border: solid 1px black;
            padding: 3px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server" />
    <div>

        <script type="text/javascript">
        var loggerObj = new Logger();
        
        function beginRequestHandler(sender, args) {
            // get WebRequest object
            var request = args.get_request();

            // get button that invoked postBack
            var clickedButton = args.get_postBackElement();
            var customData = 
                {
                    requestStart:new Date(),
                    button:args._postBackElement
                };
            // modify WebRequest, assigning custom data to _userContext property
            request.set_userContext(customData);
        };
        
        function endRequestHandler(sender, args) {
            var response = args.get_response();
            if(response.get_responseAvailable()) {
                // get associated WebRequestObject
                var webRequest = response.get_webRequest();
                // get custom data we assigned to WebRequest earlier  
                var customData =  webRequest.get_userContext();
                if(customData.button && loggerObj) {
                    var loggerDiv;
                    if(customData.button.attributes.getNamedItem("logger")) {
                        loggerDiv = customData.button.attributes.getNamedItem("logger").value;
                        var sb = new  Sys.StringBuilder();
                        
                        sb.append("----------------" + "<br />");
                        sb.append("request start: "+customData.requestStart.toTimeString() + "<br />");
                        sb.append("request end: "+new Date().toTimeString() + "<br />");
                        sb.append("request invoked by: "+customData.button.id + "<br />");
                        // display registered dataItems
                        sb.append("Data items:" + "<br />");
                        var dataItemKey = "";
                        for(dataItemKey in sender._dataItems) {
                            sb.append(dataItemKey + ": " + sender._dataItems[dataItemKey]+"<br />");
                        }
                        sb.append("----------------" + "<br />");
                        
                        loggerObj.log(loggerDiv, sb.toString());
                    }
                }
            }
        };
        
        function Logger(divId) {
            // logs a message into div
            this.log = function(logger, message) {
                if($get(logger)) {
                    $get(logger).innerHTML += message + "<br />";
                    $get(logger).scrollTop = $get(logger).scrollHeight - 400;
                }
            };
            this.clearLog = function(logger) {
                if($get(logger)) {
                    $get(logger).innerHTML = "";
                }
            };

        };
        </script>

        <asp:UpdatePanel runat="server" ID="UP">
            <ContentTemplate>
                <asp:Button ID="Button1" logger="logger1" runat="server" Text="Button1" 
                    onclick="Button1_Click" />
                <asp:Button ID="Button2" logger="logger2" runat="server" Text="Button2" 
                    onclick="Button2_Click" />
                <asp:Button ID="Button3" logger="logger3" runat="server" Text="Button3" 
                    onclick="Button3_Click" />
                <asp:Button ID="Button4" logger="logger4" runat="server" Text="Button4" 
                    onclick="Button4_Click" />
            </ContentTemplate>
        </asp:UpdatePanel>
        <div>
            <div id="logger1" class="loggerDiv">
            </div>
            <div id="logger2" class="loggerDiv">
            </div>
            <div id="logger3" class="loggerDiv">
            </div>
            <div id="logger4" class="loggerDiv">
            </div>
        </div>
    </div>
    </form>

    <script type="text/javascript">
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequestHandler);
        Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(beginRequestHandler);
    </script>

</body>
</html>

Code behind:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Threading;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Thread.Sleep(100);
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        ScriptManager1.RegisterDataItem(this, "Button1 - confirmed from the server");
    }
    protected void Button2_Click(object sender, EventArgs e)
    {
        ScriptManager1.RegisterDataItem(this, "Button2 - confirmed from the server");
    }
    protected void Button3_Click(object sender, EventArgs e)
    {
        ScriptManager1.RegisterDataItem(this, "Button3 - confirmed from the server");
    }
    protected void Button4_Click(object sender, EventArgs e)
    {
        ScriptManager1.RegisterDataItem(this, "Button4 - confirmed from the server");
    }
}

 

The most important in this code is how we attach custom object in beginRequestHandler:

var request = args.get_request();

request.set_userContext(customData);

 

And how we retrieve it later:

var response = args.get_response();

var webRequest = response.get_webRequest();

var customData = webRequest.get_userContext();

As you can see I am attaching DataItems to asynchronous responses - this is just demonstration of how can we pass some additional data from server, that can be tracked trough

sender._dataItems

property in endRequestHandler.

 

Not hard to do at all.

You can see online sample.

Also you can download source code for this sample here.

Hope this helps.

Technorati Tags: ,

Wednesday, February 20, 2008

Creating update progress indicator for AJAX enabled applications that blocks user interface while updating

This is quite needed and required thing - have a good progress indicator that the same time blocks whole screen to prevent sending several simultaneous asynchronous requests to the server. So I decided to place a code that I currently use.

Actually this code is a synthesis of codes I already have seen in some blog posts in the Internet, but it is customized to my current needs.

The indicator blocks all controls on the page while update is in progress. Also it provides two handy methods we can use to invoke progress indicator before calling web service, and hide it after response. For simple postback with UpdatePanel controls it is shown automatically.

You may want to have a look at live sample, or download source code.

Let's see the code.

JS file:

function applicationLoadHandler() {
    /// <summary>Raised after all scripts have been loaded and the objects in the application have been created and initialized.</summary>
};
function applicationUnloadHandler() {
    mainForm.CleanUp();
    mainForm = null;
    Sys.Application.dispose();
};
function beginRequestHandler() {
    /// <summary>Raised after an asynchronous postback is finished and control has been returned to the browser.</summary>
    mainForm.StartUpdating();
};
function endRequestHandler() {
    /// <summary>Raised before processing of an asynchronous postback starts and the postback request is sent to the server.</summary>
    // Set status bar text if any was passed through the hidden field on the form
    mainForm.EndUpdating()
};
var mainForm = 
{
    pnlPopup : "pnlPopup",
    innerPopup : "innerPopup",
    updating : false
};
mainForm.StartUpdating = function() {
    mainForm.updating = true;
    mainForm.AttachPopup();
    mainForm.onUpdating();
    $get(mainForm.pnlPopup).focus();
};
mainForm.EndUpdating = function() {
    mainForm.updating = false;
    mainForm.DetachPopup();
    mainForm.onUpdated();
};
mainForm.onUpdating = function(){
    if(mainForm.updating) {
        var pnlPopup = $get(this.pnlPopup);
        pnlPopup.style.display = '';        
        var docBounds = mainForm.GetClientBounds();
        var pnlPopupBounds = Sys.UI.DomElement.getBounds(pnlPopup);
        var x = docBounds.x + Math.round(docBounds.width / 2) - 
            Math.round(pnlPopupBounds.width / 2);
        var y = docBounds.y + Math.round(docBounds.height / 2) - 
            Math.round(pnlPopupBounds.height / 2);        
        Sys.UI.DomElement.setLocation(pnlPopup, x, y);
        //if(Sys.Browser.agent == Sys.Browser.InternetExplorer) {
            if(!pnlPopup.iFrame) {
                var iFrame = document.createElement("IFRAME");
                iFrame.scrolling= "no";
                iFrame.src = "nothing.txt";
                iFrame.frameBorder = 0;
                iFrame.style.display = "none";
                iFrame.style.position = "absolute";
                iFrame.style.filter = 
                    "progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)";
                iFrame.style.zIndex = 1;
                pnlPopup.parentNode.insertBefore(iFrame, pnlPopup);
                pnlPopup.iFrame = iFrame;
            } 
            pnlPopup.iFrame.style.width = docBounds.width + "px";
            pnlPopup.iFrame.style.height = docBounds.height + "px";
            pnlPopup.iFrame.style.left = docBounds.x + "px";
            pnlPopup.iFrame.style.top = docBounds.y + "px";
            pnlPopup.iFrame.style.display = "block";      
        //}  
    }           
}
mainForm.onUpdated = function() {
    // get the update progress div
    var pnlPopup = $get(this.pnlPopup);
    // make it invisible
    pnlPopup.style.display = 'none';
    if(pnlPopup.iFrame) {
        pnlPopup.iFrame.style.display = "none";
    }
}; 
mainForm.AttachPopup = function() {
    /// <summary>
    /// Attach the event handlers for the popup
    /// </summary>
    this._scrollHandler = Function.createDelegate(this, this.onUpdating);
    this._resizeHandler = Function.createDelegate(this, this.onUpdating);    
    $addHandler(window, 'resize', this._resizeHandler);
    $addHandler(window, 'scroll', this._scrollHandler);
    this._windowHandlersAttached = true;
};
mainForm.DetachPopup = function() {
    /// <summary>
    /// Detach the event handlers for the popup
    /// </summary>
    if (this._windowHandlersAttached) {
        if (this._scrollHandler) {
            $removeHandler(window, 'scroll', this._scrollHandler);
        }
        if (this._resizeHandler) {
            $removeHandler(window, 'resize', this._resizeHandler);
        }
        this._scrollHandler = null;
        this._resizeHandler = null;
        this._windowHandlersAttached = false;
    }
};
mainForm.CleanUp = function() {
    /// <summary>
    /// CleanUp all resources held by mainForm object
    /// </summary>
    this.DetachPopup();
    var pnlPopup = $get(this.pnlPopup);
    if(pnlPopup && pnlPopup.iFrame) {
       pnlPopup.parentNode.removeChild(pnlPopup.iFrame);
       pnlPopup.iFrame = null;
    }
    this._scrollHandler = null;
    this._resizeHandler = null;
    this.pnlPopup = null;
    this.innerPopup = null;
    this.updating = null;
};
mainForm.GetClientBounds = function() {
    /// <summary>
    /// Gets the width and height of the browser client window (excluding scrollbars)
    /// </summary>
    /// <returns type="Sys.UI.Bounds">
    /// Browser's client width and height
    /// </returns>
    var clientWidth;
    var clientHeight;
    switch(Sys.Browser.agent) {
        case Sys.Browser.InternetExplorer:
            clientWidth = document.documentElement.clientWidth;
            clientHeight = document.documentElement.clientHeight;
            break;
        case Sys.Browser.Safari:
            clientWidth = window.innerWidth;
            clientHeight = window.innerHeight;
            break;
        case Sys.Browser.Opera:
            clientWidth = Math.min(window.innerWidth, document.body.clientWidth);
            clientHeight = Math.min(window.innerHeight, document.body.clientHeight);
            break;
        default:  // Sys.Browser.Firefox, etc.
            clientWidth = Math.min(window.innerWidth, 
                document.documentElement.clientWidth);
            clientHeight = Math.min(window.innerHeight, 
                document.documentElement.clientHeight);
            break;
    }
    var scrollLeft = (document.documentElement.scrollLeft ? 
        document.documentElement.scrollLeft : document.body.scrollLeft);
    var scrollTop = (document.documentElement.scrollTop ? 
        document.documentElement.scrollTop : document.body.scrollTop);
    return new Sys.UI.Bounds(scrollLeft, scrollTop, clientWidth, clientHeight);
}; 
if(typeof(Sys) !== "undefined")Sys.Application.notifyScriptLoaded();

Page:

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

<!DOCTYPE html 
    PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="SM" runat="server">
        <Scripts>
            <asp:ScriptReference Path="~/Scripts/Script.js" />
        </Scripts>
    </asp:ScriptManager>
    <div style="text-align:center">
        <asp:UpdatePanel
            runat="server"
            ID="UP">
            <ContentTemplate>
                <p>Click on any button to initiate a assynchronous postback.</p>
                <p>During postback, a progress script will block any controls on a page
                so the user can not click any of the screen controls while postback is in progress.</p>
                <p>Try to resize or scroll the screen. You will notice that controls are still blocked,
                and progress div changes it's position to stay in the center of the screen</p>
                <asp:Button runat="server" Text="Postback!" ID="Button1" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button2" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button3" OnClick="Delay" /><br />
                <asp:Button runat="server" Text="Postback!" ID="Button4" OnClick="Delay" /><br />
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    <div id="pnlPopup" class="PrProgress" style="display: none;">
        <div id="innerPopup" class="PrContainer">
            <div class="PrHeader">
                Loading, please wait...</div>
            <div class="PrBody">
                <img width="220px" height="19px" 
                    src="App_Themes/Default/Images/activity.gif" alt="loading..." />
            </div>
        </div>
    </div>
    </form>
    <script type="text/javascript">
        Sys.Application.add_load(applicationLoadHandler);
        Sys.Application.add_unload(applicationUnloadHandler);
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequestHandler);
        Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(beginRequestHandler);
    </script>
</body>
</html>

 

That's it.

Hope this helps.

Technorati Tags: ,

Tuesday, February 19, 2008

Implementing IDIsposable Interface In JavaScript Class Using ASP.NET AJAX Library.

Are you so happy to already know what memory leaks can do with a browser? Did you learn how to subscribe to document's events to release resources you used in javascript class? If you use ASP.NET AJAX, it gives a couple of good ways of handling scripts that need cleanup operation.

What is cleanup?

Ok, javascript is not ideal environment to develop large code bases. There is garbage collector for javascript objects, but it has some problem when dealing with some specific situations. Check out this MSDN article by Justin Rogers to learn more. Actually we are not having trouble only with IE. Any browser has specific leaks.

This became a big problem in my current project. The project has one main page. And that page is loaded only once ! No refresh, no postback - it stays opened for whole lifecycle of a user session. There are hundreds of controls that are loading later using AJAX. And using this architecture with a lot of client-side logic, memory leaks - if not detected on early stages - can become a real headache later. Another problem is third-party scripts (calendars, sliders, etc.) that are not optimized to be used in AJAX environment, so cleanup after each usage should be very high priority task in such a project.

To solve the problem you need to release resources in some stage of page lifecycle in the browser. Normally it is onunload event of the window object.

How to write disposable class

You don't have to know the name of event  in every browser, or how to subscribe to it. We will solve this implementing on client-side IDisposable interface that is defined in ASP.NET AJAX library.

Simplest javascript class implementing IDisposable interface looks like this:

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

// Register namespace
Type.registerNamespace('MyNamespace');

//constructor
MyNamespace.MyClass = function() {
    
    // register the object as disposable, so the application will call it's dispose method when needed
    if(typeof(Sys) !== "undefined")Sys.Application.registerDisposableObject(this);
    
    // initialize class level variables from arguments
    this.MyVar1 = $get(arguments[0]);
    this.MyVar2 = $get(arguments[1]);
    
    // initialize other class level variables and constants
    this._myCssClassName = "someClass";
};

MyNamespace.MyClass.prototype = {

    dispose : function() {
        /// <summary>
        /// Implements dispose method
        /// of IDisposable interface,
        /// cleanup resources here.
        /// mainly detach events you attached earlier,
        /// assign nulls to elements that may leak etc...
        /// </summary>    
        this.MyVar1 = null;
        this.MyVar2 = null;
        alert("dispose executed!");
    },
    
    myMethod1 : function(param1, param2) {
        // TODO: implement logic
    },
    
    myMethod2 : function(param1, param2) {
        // TODO: implement logic
    },
    
    myMethod3 : function(param1, param2) {
        // TODO: implement logic
    }
};

// claim the class is implementing IDisposable
if(typeof(Sys) !== "undefined")MyNamespace.MyClass.registerClass('MyNamespace.MyClass', null, Sys.IDisposable);

// standard call for non-embedded scripts.
if(typeof(Sys) !== "undefined")Sys.Application.notifyScriptLoaded();

So the interesting part of the script is:

MyNamespace.MyClass.registerClass('MyNamespace.MyClass', null, Sys.IDisposable);

with this line we are saying - our object implements IDisposable interface.

Other important line is in the constructor of an object:

Sys.Application.registerDisposableObject(this);

this line registers our object(note - object is an instance of our class!) as disposable object. This way AJAX framework knows that it should call our dispose method when needed.

And don't forget to implement "dispose" function itself:

dispose : function() {this.MyVar1 = null; this.MyVar2 = null; alert("dispose executed!"); },

Here we made some dummy cleanup - in real application it depends on many factors. most often you will need to remove handlers from DOM elements and assign null-s to some specific variables that are the reason of memory leak.

You can read more about registerClass and registerDisposableObject on official ASP.NET AJAX documentation website.

That's it - now we can write some small test page to test the script and try to leave the page bypassing dispose method execution(you can do it only by killing a process in a task manager - but in this case you have nothing to cleanup :) )

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

<!DOCTYPE html 
    PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html 
    xmlns="http://www.w3.org/1999/xhtml">
<head 
    runat="server">
    <title>Implementing IDisposable interface</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
            <Scripts>
                <asp:ScriptReference Path="~/MyScript.js" />
            </Scripts>
        </asp:ScriptManager>
        <div>
            <input 
                type="button" 
                value="Do Something" 
                onclick="myObj.myMethod1();" />
            <p>
                Before you try to leave (or refresh) this page, you should
                see alert saying "dispose executed!".
                This means AJAX framework called our object to let us 
                cleanup the resources
            </p>
        </div>
    </form>
    <script type="text/javascript">
        var myObj = new MyNamespace.MyClass();
    </script>
</body>
</html>

Now if we try to leave the page dispose method is called in our object:

image

You can download the source code, or test it live!

Technorati Tags: ,

Thursday, February 7, 2008

Auto generate release scripts for web application using T4 template

Download source code for this article.

 

New version of VS 2008 supports advanced intellisense features, and this is definitely good. I think you are aware of this fact, if not read ScottGu's perfect article first.

Intellisense also takes care of displaying associated comments of a js functions

Take a look:

 

image

 

With these enhancements developers are encouraged to comment every javascript function, because now it helps more then before and we have the ability of maintaining well-documented code and the same time very usable.

But before releasing the script we need to remove all comments and white-spaces, to shrink the size of the files. So I want to automate this process.

To accomplish this I will use 3 files that are available from AjaxControlToolkit project:

image

They are located in Binaries sub-folder. Most of all I am interested with static "Strip" method of "JavaScriptCommentStripper" class in JavaScriptCommentStripper.dll assembly. So I am going to use this method to create release versions of all my scripts in web application. You can also search for how AjaxControlToolkit community uses the dll, I want to say shortly that the method you see here works even for scripts in simple web folder, they have not to be compiled as web resources etc.

To automate the process I will use T4 templating engine that is built into Visual Studio 2008. Please have a look of my previous article to see more detailed explanation how to use it. There are also some useful links and comments, so take a look of it.

Today I will write a little different template, because actually it will generate many files instead of one!

Lets see the picture, suppose we have a directory structure that looks like:

image

and after running transformation I want to have it look like:

image

Contents of files,

Before transformation (Script.debug.js - 6kb):

image

After transformation(Script.js - 4kb):

image

After this we can easily use nice feature of ASP.NET AJAX ScriptManager to specify the script to include, it will append .debug on the end when in debug mode:

<asp:ScriptManager 
    runat="server"
    ScriptMode="Release"
    ID="SM">
    <Scripts>
        <asp:ScriptReference ScriptMode="Inherit" Path="~/Scripts/Script.js" />
    </Scripts>
</asp:ScriptManager>

Source of page with ScriptManager running in Release mode:

<script src="Scripts/Script.js" type="text/javascript"></script>

Source of page with ScriptManager running in Debug mode:

<script src="Scripts/Script.debug.js" type="text/javascript"></script>

Ok, lets see what does this magic happen.

1) Create file in Class Library Project named "CommentStripper.tt".

2) Then add the files JavaScriptCommentStripper.dll, Tools.dll, Tools.netmodule to the same directory where template is.

3) Paste the following code into template's contents:

<#@ output extension=".cs" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#@ template hostspecific="True" #>
<#
Initialize(Host.ResolvePath(@""), Host.ResolvePath(@"..\Auto_generating_strong_typed_navigation_class"));
Process();
#>
<#+ 
    Assembly assembly;
    MethodInfo stripMethod;
    String webRootPath;
    const string extension = "*.js";
    
    public     void Initialize(string reflectionDir, string webRoot)
    {
        webRootPath = webRoot;
        string assemblyPath = System.String.Format(
            @"{0}\JavaScriptCommentStripper.dll", reflectionDir
        );
        assembly = Assembly.LoadFrom(assemblyPath);
        stripMethod = assembly
            .GetType("AjaxControlToolkit.JavaScriptCommentStripper")
            .GetMethod("Strip");
    }
    
    public void ProcessOneScript(string fileName)
    {
        string shortName = Path.GetFileNameWithoutExtension(fileName);
        if(shortName.ToLower().EndsWith(".debug"))
        {
            string debugScript = "";
            using(StreamReader sr = File.OpenText(fileName))
            {
                debugScript = sr.ReadToEnd();
            }
            
            string releaseScript = stripMethod
                .Invoke(null, new object[] { debugScript }).ToString();    
            
            string releaseScriptFileName = Path.Combine(
                Path.GetDirectoryName(fileName), 
                Regex.Replace(shortName, ".debug$", ".js")
            );
            
            using(StreamWriter sw = new StreamWriter(releaseScriptFileName))
            {
                sw.Write(releaseScript);
            }
        }
    }
    
    public void Process()
    {
        DirectoryInfo di = new DirectoryInfo(webRootPath);
        foreach (FileInfo fi in di.GetFiles(extension, SearchOption.AllDirectories))
        {
            ProcessOneScript(fi.FullName);
        }
    }
#>

Where "..\Auto_generating_strong_typed_navigation_class" should point to web application path relative to the folder of a template.

4) Click on "Run custom tool" context menu item of a template:

image

That's it !

 

Every time you click this menu item, the whole directory structure of web application will be scanned and every js file that ends with ".debug.js" will be converted to stripped version to use in release build.

The most important method here is ProcessOneScript - the method uses reflection to load and execute JavaScriptCommentStripper assembly for each file in the directory, then it just creates new (or rewrites existing) file with name of script modified by removing ".debug" in the end.

 

And I wanted to add - if you read my previous post - Auto generate strong typed navigation class for all user controls in ASP.NET web application, and use it with this scenario you can also generate strong typed representation of every JS file url:

image

To enumerate through .js files you need to modify the code I showed in previous article. I will place it here, and hope it helps:

<#@ import namespace="System.IO" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ template hostspecific="True" #> 
namespace Devarchive_Net.Navigation
{
<# 
string BaseDir = Host.ResolvePath(@"..\Auto_generating_strong_typed_navigation_class");
#>
<#=GetStrongTypedNav(BaseDir, "*.ascx", "Pages")#>
<#=GetStrongTypedNav(BaseDir, "*.js", "JsCsripts")#>
}

<#+
        public static string GetStrongTypedNav(string BaseDir, string extension, string rootClassName)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(String.Format("\tpublic static class {0}", rootClassName));
            sb.AppendLine("\t{");
            DirectoryInfo di = new DirectoryInfo(BaseDir);
            GetClasses(di, sb, 2, "~/", extension);
            sb.AppendLine("\t}");
            return sb.ToString();
        }

        public static void GetClasses(DirectoryInfo di, StringBuilder sb, int ident, string curDir, string extension)
        {
            string identString = "";
            for (int i = 0; i < ident; i++) identString += "\t";
            if (di.GetFiles(extension, SearchOption.AllDirectories).Length > 0)
            {
                foreach (DirectoryInfo diCh in di.GetDirectories())
                {
                    if (diCh.GetFiles(extension, SearchOption.AllDirectories).Length > 0)
                    {
                        int nextIdent = ident + 1;
                        string nextDir = String.Concat(curDir, diCh.Name.ToLower(), "/");
                        sb.Append(identString);
                        sb.AppendLine(String.Format("public static class {0}", diCh.Name));
                        sb.Append(identString);
                        sb.AppendLine("{");
                        foreach (FileInfo fi in diCh.GetFiles(extension, SearchOption.TopDirectoryOnly))
                        {
                            sb.Append(identString);
                            bool NAV = true;
                            using (StreamReader sr = new StreamReader(fi.FullName))
                            {
                                while (!sr.EndOfStream)
                                {
                                    string line = sr.ReadLine();
                                    if (line.Contains("<%--NONAV--%>"))
                                    {
                                        NAV = false;
                                        break;
                                    }
                                }
                            }
                            if(NAV)
                            {
                                sb.AppendLine(String.Format("\tpublic const string {0} = \"{1}\";", Path.GetFileNameWithoutExtension(fi.Name).Replace(".", "_"), String.Concat(nextDir, fi.Name)));                            
                            }
                        }
                        GetClasses(diCh, sb, nextIdent, nextDir, extension);
                        sb.Append(identString);
                        sb.AppendLine("}");
                    }
                }
            }
        }
#>

In this scenario we can use the generated scripts and generated class to add references from c# code:

using System;
using System.Web.UI;
using Devarchive_Net.Navigation;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ScriptReference sr = new ScriptReference(JsScripts.Scripts.Script);
        sr.ScriptMode = ScriptMode.Inherit;
        SM.Scripts.Add(sr);
    }
}

Hope this helps.

Technorati Tags: ,

Friday, February 1, 2008

Using "DHTML Calendar" with ASP.NET AJAX

Yesterday I lost 2 hours of my life trying to find why AJAX client library function "Date.parseInvariant(value, format)" does not work.

This is a beautiful function allows us to convert strings into Date objects in javascript. It allows us to specify exact format of a string in .NET way.

For example:

Date.parseInvariant("01/12/2008", "dd/MM/yyyy") will return valid date pointing to 1st of December 2008, and

Date.parseInvariant("01/12/20081", "dd/MM/yyyy") will return null ! Yes this is wonderful feature - there is no NaN result, no crazy dates - everything right like in .NET(almost everything :) )

 

But the function did not work. I debugged the Ajax library script and found the problem on the line:

        var result = new Date();
        if (year === null) {
            year = result.getFullYear();
        }
        if (month === null) {
            month = result.getMonth();
        }
        if (date === null) {
            date = result.getDate();
        }
                result.setFullYear(year, month, date);
                if (result.getDate() !== date) return null;
                if ((weekDay !== null) && (result.getDay() !== weekDay)) {
            return null;
        }

After execution of line

result.setFullYear(year, month, date);

the result date was not equal to date we just assigned it!

After long searches and guesses I have found the following script block in javascript file used by "The DHTML Calendar" by www.dynarch.com :

Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear;
Date.prototype.setFullYear = function(y) {
    var d = new Date(this);
    d.__msh_oldSetFullYear(y);
    if (d.getMonth() != this.getMonth())
        this.setDate(28);
    this.__msh_oldSetFullYear(y);
};

// END: DATE OBJECT PATCHES

The "Object patch" patched too much. It replaced setFullYear standard Date javascript function with it's own, completely ignoring two additional parameters that are passed into it.

 

I solved it by removing the "patch" - commenting it out. It worked for me, as I tested no critical functionality was broken for simple calendar dates selection. You can try to remove/replace the "patch" with more care, I just want to point to this problem with this post.

And in the end I want to thank the dynarch.com team - the calendar is great control anyway. I like it and use in my personal projects. Just yesterday was a little angry about this issue :P.

 

Hope this helps.

Technorati Tags:

Serializing DateTime values using JavaScriptSerializer class.

I use this class intensively in my current AJAX-enabled web application. The class serializes/deserializes C# class into JSON, and can deserialize it back.

To see more details about the class you can see my previous post: Passing complex objects between client browser and web server using ASP.NET AJAX framework or official ASP.NET AJAX documentation.

I have the problem with correct deserialization of DateTime types in my custom serializable classes.

To show the problem clearly I created the following test console application:

public class SimpleClass
{
    private DateTime m_Date;

    public DateTime Date
    {
        get { return m_Date; }
        set { m_Date = value; }
    }
}
 class Program
 {
     static void Main(string[] args)
     {
         JavaScriptSerializer serializer = new JavaScriptSerializer();

         DateTime dt = DateTime.Now;
         SimpleClass instance = new SimpleClass();
         instance.Date = dt;

         Console.WriteLine(
             "Before serialization: {0}",
             instance.Date);

         string jsonStr = serializer.Serialize(instance);

         SimpleClass newInstance = serializer.Deserialize<SimpleClass>(jsonStr);

         Console.WriteLine(
             "After serialization: {0}", 
             newInstance.Date);
     }
 }

We should expect to see the same value for instance.Date and newInstance.Date objects, but instead we see that:

image

for initial DateTime value equal to DateTime.MinValue:

image

DateTime.MaxValue:

image

So do you see the problem? Dates are different before and after serialization. This is definitely wrong.

I downloaded source code for ASP.NET AJAX library and created a simple library to debug the issue with comfort. And I found the line creating the wrong DateTime value. This is done during deserialization. The following listing shows the code:

image

The problem exists because of different Kind's of DateTime objects before and after serialization. As you see in last picture, AJAX framework deserializes DateTime value assuming it's Kind property should be set to Utc format. But the original DateTime value was set to Local format. It means that for me (I am in +4:00 time zone) the local time is greater then a universal time by 4 hours, and this makes a big difference. If a user tries to select 2/2/2008 at 1:00 AM - he gets 2/1/2008. This is bad.

Lets explore how the Kind property of a DateTime values are assigned.

By default when you assign a value by using statement:

DateTime dt = DateTime.Now;

The Kind property of a dt variable is set to Local:

image

And if we assign it in the way:

DateTime dt = DateTime.UtcNow;

We get:

image

And finally if we assign the value:

DateTime dt = DateTime.MinValue;

image

You can see more information about DateTime.Kind property on MSDN.

So I started to think (the good thing) about why AJAX library deserializes DateTime's to Utc without persisting the Kind property.

The first idea was this is a bug.

Then I started to think how to persist the Kind property in the class to be able to assign it to DateTime value after deserialization have occurred, but in this case we must implement also a functionality to create a Date object in javascript from custom serialized data ourselves.

Anyway what is the purpose of JSON? JSON gives the ability to pass objects to the client side, thus it will be sent over the network. The size of JSON is also critical, it must be as small as possible.

I changed the class we serialize as follows:

public class SimpleClass
    {
        private DateTime m_Date;

        public DateTime Date
        {
            get { return m_Date; }
            set { m_Date = DateTime.SpecifyKind(value, DateTimeKind.Utc); }
        }
    }

Using this syntax I force the m_Date field to contain only Utc dates. And it works!

image

DateTime.MinValue:

image

DateTime.MaxValue:

image

One thing we should keep in mind - with this change we are forcing Date property of state class to contain Utc date kind, this means we will not be able to know real initial Kind of date. and if you need some manipulation over Date values converting them to Utc or Local in deserialized object, - this might not work for you.

Technorati Tags: