2670

ASP.NET Futures: Control ClientID Generation

ASP.NET is expected to have some improvements on the generation of client IDs.

Although this is a major improvement, it comes short by not allowing the generation of shorter client IDs for server controls.

My good friend Nuno Gomes has done some work on generating shorter client IDs for controls (*).

Jeff has taken it one step further with his How to shorten ASP.NET automatically generated control IDs article on CodeProject.

If you want to see a running example, check out http://www.biocompare.com/.

How To Issue Server Callbacks

Callbacks were introduced in ASP.NET 2.0 and is a simple mechanism for calling page or control functionality without page rendering and without the user noticing a post back.

For a page or control to handle callbacks, all it needs is to implement the ICallbackEventHandler Interface.

When the client calls back to de page or control, the initial state of the controls is posted along with the control being called upon in the __CALLBACKID field and the callback parameter in the __CALLBACKPARAM field.

It’s quite a simple procedure.

But what if you want to issue a callback server side?

In order for a request to be identified as a callback (IsCallback), the request must be a postback (IsPostback) and the before mentioned fields must be in the post data of the request. On the other hand, for a request to be considered a postback, the level of server calls (Transfer or Execute) must be 0 (meaning that the current request hasn’t made any Transfer or Execute calls) or the type of the page is the same of the Handler for the current request and the HTTP method is POST.

Changing the HTTP method is (as far as I know) impossible. So, if the request is not already a POST, there’s no way to issue a callback.

Setting the post data is easier. All it’s needed is to override the page’s DeterminePostBackMode method (or in a page adapter) and return the post data previously saved in a context item. Something like this:

protected override NameValueCollection DeterminePostBackMode()
{
    NameValueCollection postBackMode = Context.Items["callbackPostData"] as NameValueCollection;

    return (postBackMode != null) ? postBackMode : base.DeterminePostBackMode();
}

And issue a callback is something like this:

IHttpHandler handler = this.Context.Handler;
try
{
    NameValueCollection postData = new NameValueCollection();
    postData.Add("__CALLBACKID", sender);
    postData.Add("__CALLBACKPARAM", this.argument.Text);

    Context.Items["callbackPostData"] = postData;

    Page calledPage = (Page)PageParser.GetCompiledPageInstance("~/Callback1.aspx", this.Server.MapPath("~/Callback1.aspx"), this.Context);

    this.Context.Handler = calledPage;

    StringWriter writer = new StringWriter();

    Server.Execute(calledPage, writer, false);

    this.response.Text = writer.ToString();
}
finally
{
    this.Context.Handler = handler;
}

You can find an implementation of a caller and a called page here.

Serialized In-Process ASP.NET Session State Store

 


ASP.NET provides out of the box three session state stores:
















Provider Description
InProc

Session state is stored in the ASP.NET cache.

SQLServer

Session state is stored out-of-process in an SQL Server database.

StateServer

Session state is stored out-of-process in an ASP.NET state service.


Because with SQLServer and StateServer providers the state must cross the AppDomain boundary it needs to be serialized when stored and deserialized when loaded. Because the state needs to be loaded and stored on each request, it is only available from the PostAcquireRequestState to the ReleaseRequestState events. And, because of serialization and deserialization, all objects stored must be serializable any reference held to one of the session state items won’t be to the same item after being deserialized.


On the other hand, with the InProc provider, the state will never be serialized or deserialized, which means that objects don’t need to be serializable and any reference to an item in the state is always a reference to the item in the state even before the PostAcquireRequestState event and after the ReleaseRequestState event.


In practice, developers use the InProc provider and applications are deployed to production using the SQLServer provider. This usually leads to application errors, like storing non serializable objects in the state, that are only uncovered in production. That’s why I’ve written a serializable in-process session state store. You can find the sources here.

IIS/ASP.NET Cookieless Support Not Working As Expected

In one of the environments I work, cookies cannot be used because the pages run inside web browser controls running on a client application and cookies end up being shared by all browsers.

Fortunately, ASP.NET allows us to persist some cookies as part of the URL.

To persist the session state identifier cookie in the URL we just need to add the following configuration:

<configuration>
  <system.web>
    <sessionState cookieless="UseUri" />
  </system.web>
</configuration>

and you’ll get URLs like this:

http://localhost/Cookieless/(S(jcmwek3ja0lvdpbwoacpjirv))/default.aspx

The way IIS and ASP.NET do this is by IIS removing the section between parenthesis after the virtual directory path and adding the AspFilterSessionId HTTP header to the request. Than, ASP.NET picks it up and extracts the cookie.

I wrote this simple page to demonstrate this working:

<%@ Page Language="C#" AutoEventWireup="true" %>

<!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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>
            <tr>
                <td>Raw URL</td>
                <td><%= Request.RawUrl %></td>
            </tr>
            <tr>
                <td>Cookiless Cookies<br />AspFilterSessionId Request HTTP Header</td>
                <td><%= Request.Headers["AspFilterSessionId"] %></td>
            </tr>
            <tr>
                <td>Session ID</td>
                <td><%= Session.SessionID %></td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

For the above URL, we'll get a page like this:

Raw URL /Cookieless/default.aspx
Cookiless Cookies

AspFilterSessionId Request HTTP Header
S(jcmwek3ja0lvdpbwoacpjirv)
Session ID jcmwek3ja0lvdpbwoacpjirv

IIS strips these cookies even for serving static content like cascading stylesheets.

You can test this by creating a default theme. You can do this by adding a Default folder under the App_Themes folder and adding a Styles.css file to it:

body
{
    background-color: Yellow;
}
table, tr, td
{
    border: thin solid black;
}

and setting the theme as default using the following configuration:

<configuration> <system.web> <sessionState cookieless="UseUri" /> <pages theme="Default"/> </system.web> </configuration>

And you'll get a "pretier" page:

Raw URL /Cookieless/default.aspx
Cookiless Cookies

AspFilterSessionId Request HTTP Header
S(jcmwek3ja0lvdpbwoacpjirv)
Session ID jcmwek3ja0lvdpbwoacpjirv

If you have special needs for your session state identifiers, you can implement your own session identifier manager.

But if you want to use cookieless cookies, you only have one way to do it: extend the SessionIDManager class:

public class SessionIdManager : System.Web.SessionState.SessionIDManager
{
    public override string CreateSessionID(System.Web.HttpContext context)
    {
        string id = System.Guid.NewGuid().ToString("B");

        return id;
    }

    public override bool Validate(string id)
    {
        try
        {
            new System.Guid(id);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

and configure the session state module to use it:

<configuration>
  <system.web>
    <sessionState cookieless="UseUri" sessionIDManagerType="SessionIdManager" />
    <pages theme="Default"/>
  </system.web>
</configuration>

And we'll end up with this nice page:

http://localhost/Cookieless/(S(%7b0861e55a-e29b-4b6f-825b-1e1d4c57f095%7d))/default.aspx

Raw URL /Cookieless/(S({0861e55a-e29b-4b6f-825b-1e1d4c57f095}))/default.aspx
Cookiless Cookies

AspFilterSessionId Request HTTP Header
 
Session ID {0861e55a-e29b-4b6f-825b-1e1d4c57f095}

OOPS! What happened here?

Looks like IIS was unable to transfer the cookies to the appropriate HTTP header but ASP.NET was able to find the requested resource. On the other hand, IIS couldn’t find the http://localhost/Cookieless/(S(%7b0861e55a-e29b-4b6f-825b-1e1d4c57f095%7d))App_Themes/Default/Styles.css.

This happens in these environments:

Operating System IIS ASP.NET
Windows XP Pro SP3 5.1 2.0 SP1, 3.5
Windows Server 2003 R2 6 2.0 SP1, 3.5
Windows Server 2008 7 2.0 SP1, 3.5

Fortunately, in IIS 7 you can have HTTP modules in integrated pipeline mode that are called for every resource requested to IIS.

Your module doesn’t even need to do nothing. It just needs to exist:

public class Module : System.Web.IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(System.Web.HttpApplication context)
    {
    }
}

and be added to the configuration:

<configuration>
  <system.web>
    <sessionState cookieless="UseUri" sessionIDManagerType="SessionIdManager" />
    <pages theme="Default"/>
  </system.web>
  <system.webServer>
    <modules>
      <add name="Module" preCondition="integratedMode" type="Module" />
    </modules>
  </system.webServer>
</configuration>

And our “pretty” page is back:

Raw URL /Cookieless/default.aspx
Cookiless Cookies

AspFilterSessionId Request HTTP Header
S({0861e55a-e29b-4b6f-825b-1e1d4c57f095})
Session ID {0861e55a-e29b-4b6f-825b-1e1d4c57f095}

Is it just me, or there’s something definitely wrong here?

That’s why I opened this bug on Microsoft Connect

Other Ways For Making PathInfo And ASP.NET Themes Work Together

On my last post I wrote about a solution for the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.

Raj Kaimal suggested rewriting all LINK HTML elements URLs to the correct URL as seen from the client. Something like this:

void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
    var httpApplication = sender as System.Web.HttpApplication;

    var httpContext = httpApplication.Context;

    var page = httpContext.CurrentHandler as System.Web.UI.Page;

    if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
    {
        page.PreRenderComplete += delegate(object source, System.EventArgs args)
        {
            var p = source as System.Web.UI.Page;

            foreach (System.Web.UI.Control headerControl in p.Header.Controls)
            {
                var link = headerControl as System.Web.UI.HtmlControls.HtmlLink;
                if (link != null)
                {
                    link.Href = p.ResolveUrl(link.Href);
                }
            }
        };
    }
}

With this approach you still have a problem (which mine didn’t solve) with post backs because the rendering of the ACTION of the HTML FORM is also broken.

Israel Aéce suggested the use of the BASE HTML element to re-base relative URLs. Something like this:

void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
    var httpApplication = sender as System.Web.HttpApplication;

    var httpContext = httpApplication.Context;

    var page = httpContext.CurrentHandler as System.Web.UI.Page;

    if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
    {
        page.PreRenderComplete += delegate(object source, System.EventArgs args)
        {
            page.Init += delegate(object source, System.EventArgs args)
            {
                var p = source as System.Web.UI.Page;

                var htmlBase = new System.Web.UI.WebControls.WebControl(System.Web.UI.HtmlTextWriterTag.Base);
                htmlBase.Attributes.Add("href", p.Request.Url.GetLeftPart(System.UriPartial.Authority) + p.Request.CurrentExecutionFilePath);
                p.Header.Controls.Add(htmlBase);
            };
        };
    }
}

This seems like the better solution except if your site sits behind several security perimeters and it is not possible to be sure what the domain is as seem from the client side, which was my problem to begin with.

But if you are thinking of calling Server.Execute, Server.TransferRequest or Server.TransferRequest, neither of these two solutions will work.

Making PathInfo And ASP.NET Themes Work Together


Updated on 2008.07.28 – The code was done in a hurry and, talking to my friend Luís, I noticed that I had forgotten to make a case insensitive comparison and that the code was not so obvious. So, I updated the code and added an explanation.


On my last post I wrote about the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.


But most of the times you don’t care about the why you can’t. You just want to know how you can.


The way I see it, the right solution would be to render the URLs for the stylesheets rooted.


But since I can’t do that, the next best thing is the serve the wrongly addressed request properly.


But how can we do that?


The only way I could come up with, was an HTTP Module:

public class AppThemesModule : global::System.Web.IHttpModule
{
private const string LocalThemesFolderName = "/App_Themes/";
private static readonly int searchStartIndex;
private static readonly int minimumLenghtForSearch;

static AppThemesModule()
{
int searchStartIndex = System.Web.HttpRuntime.AppDomainAppVirtualPath.Length;

AppThemesModule.searchStartIndex = ((searchStartIndex == 1) ? 0 : searchStartIndex) + 2;

AppThemesModule.minimumLenghtForSearch = AppThemesModule.searchStartIndex + AppThemesModule.LocalThemesFolderName.Length;
}

#region IHttpModule Members

public void Dispose()
{
}

public void Init(System.Web.HttpApplication context)
{
context.BeginRequest += HttpApplicationBeginRequest;
}

#endregion

void HttpApplicationBeginRequest(object sender, System.EventArgs e)
{
System.Web.HttpApplication httpApplication = sender as System.Web.HttpApplication;

string path = httpApplication.Request.Path;
if (path.Length > searchStartIndex)
{
int appThemesStartIndex = path.IndexOf(AppThemesModule.LocalThemesFolderName, searchStartIndex, System.StringComparison.OrdinalIgnoreCase);
if (appThemesStartIndex > 0)
{
httpApplication.Context.RewritePath("~" + path.Substring(appThemesStartIndex));
}
}
}
}


The code starts by initializing the static read-only field searchStartIndex with the start index of the search for the /App_Themes/ pattern. If the length of the application’s virtual path is 1, that means that it’s the root of the site and search start index is 0 instead of 1; otherwise the search start index will be the length of the application’s virtual path. 2 is added because there is no need to start searching the path just after the application’s virtual path (if the pattern was found just after the application’s virtual path, no replacement would be needed).


Than, the static read-only field minimumLenghtForSearch is initialized with the minimum length of the path to search for the pattern. There is no need to search for the pattern on paths shorter than the search start index plus the length of the pattern because, if found, no replacement would be needed.


Besides registering the module, you’ll have to configure your virtual directory so that all the files to be served out of the themes are handled by a StaticFileHandler.

PathInfo And ASP.NET Themes: Why Can’t They Work Together?

If you ever worked with ASP.NET Themes and Skins, you know that stylesheet links are added to the head section of the HTML document.

The rendered URL to these stylesheets is always relative to location of the page being requested.

So, for a request to:

http://MySite/Section/Default.aspx

you'll get:

<link href="../App_Themes/Default/Styles.css" type="text/css" rel="stylesheet" />

which will make the web browser request for:

http://MySite/App_Themes/Default/Styles.css

and it all works fine.

Well, it works fine until you need to navigate to:

http://MySite/Section/Default.aspx/PathInfo

You'll get the same stylesheet reference and the browser will request for:

http://MySite/Section/Default.aspx/App_Themes/Default/Styles.css

This happens because the web browser has no knowledge of what PathInfos are. It only accounts for the number of forward slashes (/).

I've filed a bug on Microsoft Connect about this.

Make The HttpValueCollection Class Public And Move It To System.DLL

I find the System.Web.HttpValueCollection class very useful in a wide number of situations that involve composing HTTP requests or any other need to represent name/value collection as a string (in an XML attribute, for example).

As of now (.NET Framework 3.5 SP1 Beta), the only way to create an instance of the System.Web.HttpValueCollection class is using the System.Web.HttpUtility.ParseQueryString method.

I’d like to see it public and available in a more generic assembly like System.DLL to be available on every type of .NET application (Windows Client applications, Windows Service applications, Silverlight applications, Windows Mobile applications, etc.).

If you agree with me, vote on my suggestion on Microsoft Connect.

BEWARE: System.Web.HttpValueCollection Parsing Is Not Reversible

If you run this code:

System.Collections.Specialized.NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString("noKey&=emptyKey&A=Akey");

queryString will actually have the running type of System.Web.HttpValueCollection.

What's great about this class is that its ToString method output is the collection's content in a nice URL encoded format.

As with its base class (NameValueCollection), there’s a difference between a null string and an empty string key and the parsing treats query string parameters with no parameter specification as having a null string key and parameters with an empty string key having an empty string parameter key.

So, when call ToString on the instance returned by System.Web.HttpUtility.ParseQueryString method you would expect to get the parsed string (or, at least, one that would be parsed into the equivalent collection), right? But what you’ll get instead is this: noKey&emptyKey&A=Akey.

I’ve filed a bug into connect. If you think this is important and must be corrected, please vote.

Isolator 4.3 Released!

Today Typemock released version 4.3 of Typemock Isolator. Download it from here.

What’s new?

  • Support for Ivonna. For those of you who develop ASP.Net applications, Ivonna is a great tool, built on top of Isolator’s platform, to simplify writing tests for ASP.NET.
  • Typemock.Integration.Packs namespace APIs added to support license management through Isolator, the way Ivonna does.
  • As announced when it was released, version 4.2 was the last version of Isolator to support .NET 1.1. Version 4.3 only supports the 2.0 runtime and its Visual Studio counterparts: VS2005 and VS2008.
  • For 64 bit machines, now there’s a single installer. (don’t forget to uninstall both previous 32 and 64 installers prior to installing 4.3.)
  • RecorderManager.GetMockOf(instanceRef) and MockManager.GetMockOf(instanceRef). To retrieve the mock controller object based on a reference to the instance. (more...).

Bug fixes:

  • Fixes to DLINQ support. LINQ Queries with data tables now work better, for example with GroupBy.
  • Static constructors in Natural Mocks are now invoked correctly.
  • A bug that caused an exception to be thrown when mocking interfaces ("Method XX in type IMyInterface has no matching overload that returns TypeMock.Mock+a) was fixed.
  • A bug that caused an exception to be thrown when the mocked object was overriding Equals was fixed.
  • A bug that caused failure in mocking explicit interface with the same name was fixed.
  • A bug occurring im multithreading scenarios in VerifyWithWait was fixed.
  • A bug that causes NullReferenceException to be thrown when using Auto Deploy was fixed.

See also: