Changing the autogenerated password format in the SqlMembershipProvider

Since working with ASP.Net 2.0, I’ve been using the included Membership API almost exclusively. There was one piece of it I was never crazy about: the autogenerated passwords created by the SqlMembershipProvider (and, presumably, other providers).

Let me rephrase that. I didn’t have a problem with the auto-generated passwords, which included many symbol characters. For me, it was simple enough to copy/paste the autogenerated password (which would look something like this: @&#eyue@^%#), log in, then change the password to something more memorable.

Unfortunately, I am not a good representative sample of the typical web user, and eventually I heard my clients complain about the “overly complex” autogenerated passwords. Despite the explanations of an easy workaround (copy and paste) and the added benefit of the complex passwords (more security), clients still complain.

I came up with a quick way to hack the SqlMembershipProvider to generate less complex passwords, and it was as simple as creating a new provider class that inherits from SqlMembershipProvider, then overriding the GeneratePassword method.

In the code sample below, the autogenerated password will create a fixed number of randomly generated symbols (based on the MinRequiredNonAlphanumericCharacters property), then add a series of randomly generated upper case letters to ensure a password of at least MinRequiredPasswordLength characters long (with a forced minimum of eight characters).

Using only upper case letters is just one example. You could just as easily use random digits (zero through 9), or random lower case letters. There is an argument for using random lower case letters instead of upper case letters, as people are apt to interpret an upper case “O” as a zero “0”, a problem that generally doesn’t exist with lower case letters.

Some day I may hash this out to be a more robust provider with more options on choosing the password formats, but for the immediate problem, it’s more than adequate.

namespace CustomProviders
{
public class ExtendedSqlMembershipProvider : System.Web.Security.SqlMembershipProvider
{
public ExtendedSqlMembershipProvider() : base()
{
}
public override string GeneratePassword()
{
Random rand = new Random();
StringBuilder newpassword = new StringBuilder();
int length = (this.MinRequiredPasswordLength < 8 ? 8 : this.MinRequiredPasswordLength); int symbollength = this.MinRequiredNonAlphanumericCharacters; int charlength = length - symbollength; for (int index = 1; index <= symbollength; index++) { newpassword.Append((char)rand.Next(33, 47)); } for (int index = 1; index <= charlength; index++) { newpassword.Append((char)rand.Next(65, 90)); } return newpassword.ToString(); } } } [/source] Apparently, this doesn't work in all situations. According to Lutz Roeder's Reflector, the CreateUserWizard control calls this: [source='c-sharp'] if (this.AutoGeneratePassword) { int num1 = Math.Max(10, Membership.MinRequiredPasswordLength); this._password = Membership.GeneratePassword(num1, Membership.MinRequiredNonAlphanumericCharacters); } [/source] The Membership class (a static class) has its own password hashing algorithm that ignores the hashing of the provider. Of course, for consistency, the PasswordRecovery control calls the provider's ResetPassword() method, which in turn calls the provider's GeneratePassword() method. This matches my testing where the custom provider outlined above will generate passwords in the alternate format when being reset (using the PasswordRecovery control), but initial passwords are in the original format (thanks to the CreateUserWizard control). In my opinion, this is a faulty implementation -- a provider system which is ignored in one respect and recognized in another. It should not be this way.

CSS Adapters for Membership Controls (working versions)

On September 6, the guys at Microsoft released the second beta version of their CSS Friendly ASP.Net 2.0 Control Adapters. In addition to bug fixes were CSS implementations of the membership controls – specifically, the Login, CreateUserWizard, PasswordRecovery, LoginStatus, and ChangePassword controls.

Unfortunately for the membership controls, it didn’t really work. This bothered me, because I’ve been using a hacked version of these controls that strips out TABLE-related tags for some time. The hacks work, but they’re hacks, and they have some quirky behavior.

I wrote a post on the ASP.Net forums, in which I described the problem.

I was glad to see that membership controls were added to the latest beta release. However, I did notice that all of them (with one exception) still put a TABLE wrapper around the content when using a templated control. It happens on the Login and PasswordRecovery controls…

Ironically, the CreateUserWizard did not add a table wrapper around the CreateUserWizardStep — but it did add it around the CompleteWizardStep.

Russ Helfand (obviously one of the guys working on the CSS adapters) responded and gave some tips. After some trial and error, we figured out some implementation updates that fix the problem for the Login, CreateUserWizard, and PasswordRecovery controls — an approach that can likely be used to fix other “broken” CSS adapters, too. A snippet of the solution follows.

Russ Helfand suggested:

Let’s look at the LoginAdapter as an example, http://www.asp.net/CSSAdapters/srcviewer.aspx?inspect=%2fCSSAdapters%2fMembership%2fLogin.aspx. In particular, I want you to look at line 125 where the template container is being rendered. I’m considering changing that from:

container.RenderControl(writer);

To:

foreach (Control c in container.Controls)
{
c.RenderControl(writer);
}

It would be helpful if you could try that fix out locally and let me know if it works well for you.

I tried it, and it worked!

I used a similar approach for other controls. Take the CreateUserWizard adapter as an example. Since this adapter only added the table in the Completed step, I made the following change in the RenderContents() method (around line 181):

        activeStep.RenderControl(writer);

… changes to:

        if (activeStep.StepType == WizardStepType.Complete)
            foreach (Control c in activeStep.Controls[0].Controls[0].Controls[0].Controls[0].Controls)
            {
                c.RenderControl(writer);
            }
            else
                activeStep.RenderControl(writer);

For the PasswordRecovery adapter, there’s three places to change — each instance of the RenderControl() method in the RenderContents() method should be commented out and replaced by the foreach loop.

    //passwordRecovery.UserNameTemplateContainer.RenderControl(writer);
    foreach (Control c in passwordRecovery.UserNameTemplateContainer.Controls)
        c.RenderControl(writer);

    ...

    //passwordRecovery.QuestionTemplateContainer.RenderControl(writer);
    foreach (Control c in passwordRecovery.QuestionTemplateContainer.Controls)
        c.RenderControl(writer);

    ...

    //passwordRecovery.SuccessTemplateContainer.RenderControl(writer);
    foreach (Control c in passwordRecovery.SuccessTemplateContainer.Controls)
        c.RenderControl(writer);

That’s some weird behavior, but it works, and it doesn’t seem to break any of the functionality. Be sure to check out the complete thread on the ASP.Net forums for details.

Mapping business objects to ASPX pages

While going through the ASP.Net forums, I stumbled across a post where a guy asked:

I’m having trouble coming up with a good solution for programmatically mapping members of a business object to control elements on our UI. I’m wondering if someone else has done something similar and might have some useful suggestions.

My reply was a rather lengthy one in which I talked about an approach I recently used in the rewrite of my online baseball game, CSFBL. Since I eventually wanted to blog about this stuff, I figured I’ll just copy/paste the post I made on the forums below…


I struggled with this a bit (struggled in the sense that I couldn’t come to agreement with myself on how to do it!)… the following is what I settled on.

All my business objects are created using CodeSmith templates for Paul Wilson’s O/R mapper. I wanted the web interface to be able to use these directly from both the code-behind and the ASPX page. (Yes, I know the deal about allowing your presentation to be able to edit your data; I’m the only one working on the project, and I can easily refactor out that functionality to factory classes later.

The main reason the approach below worked on my project was because any given web page displayed the properties accessed through a single business object. For example, if this was a contact management web app (it isn’t), an instance of the Contact business class would be effectively “bound” to the ContactDetails.aspx web page. Note that the approach below can also be tailored to allow similar behavior with user controls (i.e. a user control is ‘bound’ to a specific business object).

To do all this required the creation of a few new classes in the web project.

The PageBase class overrides the default Page class, and adds one key function: a call to the page’s DataBind() method in the PreRender stage. This does two things: (1) it eliminates the need to call a DataBind() class in your page’s objects (at least I haven’t found anything that required otherwise), and (2) it enables us to bind in the ASPX (more on that soon).

public class PageBase : System.Web.UI.Page
{
 protected override void OnPreRender( EventArgs e )
 {
 this.DataBind();
 base.OnPreRender(e);
 }
}

The IDataView generic interface exposes a LoadData method and a simple “Data” property.

interface IDataView<t>
{
 T Data { get; }
 void LoadData();
}

The DataViewPage inherits our PageBase and implements the IDataView interface.

public abstract class DataViewPage : PageBase, IDataView
{
 private T _data;
 public T Data
 {
 get { return _data; }
 set { _data = value; }
 }

 protected override void OnInit( EventArgs e )
 {
 this.LoadData();
 if ( this._data == null )
 throw new DataLoadException("Unable to load data for the current page.");

 base.OnInit(e);
 }

 public abstract void LoadData();
}

Finally, a real web page. The web app itself is for a baseball game (the next iteration of CSFBL), so the sample below is the binding of a baseball league to the “LeagueAdmin” web page.

public partial class LeagueAdmin : DataViewPage
{
 public League League
 {
 get { return this.Data; }
 }

 public override void LoadData()
 {
 this.Data = League.RetrieveByKey(1);
 }
}

There’s a few key points to note on how I created the codebehind for the web page:

  • I exposed the Data property (which, thanks to generics, would still be the correct object type, in this case League) using a name other than Data (i.e. I used the property name League). This is merely for convenience and code readability.
  • The LoadData() method (from the interface) populates the Data property by loading from the business model (RetrieveByKey is a method on the League object that returns an instance of the League with the given key).

By doing this, I can do the following in my ASPX page:

Note that I used inline databinding < %# %> and not just < %= %>. The main reason for this is that the inline databinding does not get called until the OnPreRender event (remember our PageBase class?), which helps in situations where you have a postback that changes the business object data — by using databinding I was able to guarantee that this happened after any event-related data changes.

Now I don’t know if this is a great thing or a bad thing, but it definitely solved some problems for me. Oh, and note that all pages on the web site also work with viewstate turned off (another one of my requirements).

I hope I explained all that clearly!

Removing the TABLE from the CreateUserWizard control

About two months ago, I wrote a post about removing the TABLE from ASP.Net 2.0’s Login control. The below code will let you do the same from the CreateUserWizard control. One important caveat: for this to work (in my limited testing), you must provide a custom template for the ContentTemplate and CustomNavigationTemplate of the CreateUserWizardStep, and for the ContentTemplate of the CompleteWizardStep.

public class CssCreateUserWizard : System.Web.UI.WebControls.CreateUserWizard
{
    protected override void Render( HtmlTextWriter writer )
    {
        if ( CreateUserStep.ContentTemplate != null && this.ActiveStep == this.CreateUserStep )
        {
            WebControl creatediv = new WebControl( HtmlTextWriterTag.Div );
            creatediv.CssClass = this.CssClass;
            CreateUserStep.ContentTemplate.InstantiateIn( creatediv );
            CreateUserStep.ContentTemplateContainer.Controls.Clear();
            CreateUserStep.ContentTemplateContainer.Controls.Add( creatediv );
            creatediv.RenderControl( writer );

            if ( CreateUserStep.CustomNavigationTemplate != null )
            {
                WebControl navdiv = new WebControl(HtmlTextWriterTag.Div);
                navdiv.CssClass = this.CssClass;
                CreateUserStep.CustomNavigationTemplate.InstantiateIn(navdiv);
                CreateUserStep.CustomNavigationTemplateContainer.Controls.Clear();
                CreateUserStep.CustomNavigationTemplateContainer.Controls.Add(navdiv);
                navdiv.RenderControl(writer);
            }
        }

        if ( CompleteStep.ContentTemplate != null && this.ActiveStep == this.CompleteStep )
        {
            WebControl completediv = new WebControl( HtmlTextWriterTag.Div );
            completediv.CssClass = this.CssClass;
            CompleteStep.ContentTemplate.InstantiateIn( completediv );
            CompleteStep.ContentTemplateContainer.Controls.Clear();
            CompleteStep.ContentTemplateContainer.Controls.Add( completediv );
            completediv.RenderControl( writer );
        }
    }
}

A URL rewriting library for ASP.Net 2.0

While working on a client project, I needed some URL rewriting code. Effectively, I wanted to turn this:

/4/Page.aspx?

… into this:

/Page.aspx?id=4

Sure, I’ve done URL rewriting in the past using the Application_BeginRequest method, but I wanted something more modular, scalable, and powerful. What I found was http://urlrewriting.net/en/Default.aspx.
That site offers a fantastic, free, open-source solution for ASP.Net 2.0 Web sites that need URL rewriting functionality. Implemented as an HTTP handler, it uses regular expressions for maximum power and flexibility, and it only takes a few minutes to integrate with your project. Highly recommended!

Removing tables in Microsoft’s ASP.Net 2.0 Controls

Microsoft has been touting ASP.Net 2.0 as taking great strides towards XHTML compatibility and meeting common accessibility guidelines. However, some controls still add table tags around them, despite your best efforts to avoid using tables. The new Login control is one of those.

For me, the table wrapper around the Login control was causing rendering problems. I was using a pure CSS layout with no tables for the login form, and the presence of the table tag wrapped around the control was breaking my layout. A quick Google search (q=asp.net+2.0+login+control+table) found a blog post that outlined a solution.

The fix was simple: create a new class, inherit from the existing System.Web.UI.WebControls.Login class, and override the Render method. A few lines of code, and the table wrapper was replaced with a div wrapper. There was one catch: the version posted on the aforementioned blog didn’t include the CssClass attribute on the new control. One extra line of code fixed that.

Below is the code, all but one line thanks to Alex Gorbatchev of dreamprojections.com:

    public class CssLogin : System.Web.UI.WebControls.Login
    {
        protected override void Render( HtmlTextWriter writer )
        {
            WebControl div = new WebControl( HtmlTextWriterTag.Div );

            LayoutTemplate.InstantiateIn( div );

            Controls.Clear();
            Controls.Add( div );

            div.CopyBaseAttributes( this );
            div.CssClass = this.CssClass;
            div.RenderControl( writer );
        }
    }

ASP.Net, CDO, and ‘The transport failed to connect to the server.’

A Web application (written in ASP.Net 1.1) that has run with no problems for months suddenly started giving a problem. The problem occurred when running the following code:

SmtpMail.SmtpServer = ConfigurationSettings.AppSettings["SmtpServer"];

MailMessage msg = new MailMessage();
msg.BodyFormat = MailFormat.Text;
msg.From = ConfigurationSettings.AppSettings["returnEmailAddress"];
msg.To = formEmail.Value;
msg.Bcc = ConfigurationSettings.AppSettings["supportEmailAddress"];
msg.Subject = "My message subject";
msg.Body = FormatEmailBody();

SmtpMail.Send( msg );

You would think that would work fine, but it didn’t. At the SmtpMail.Send() command, the following error was thrown:

Server Error in ‘/’ Application.


The transport failed to connect to the server.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Runtime.InteropServices.COMException: The transport failed to connect to the server.
Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:

[COMException (0x80040213): The transport failed to connect to the server.]

[TargetInvocationException: Exception has been thrown by the target of an invocation.]
System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters) +0
System.RuntimeType.InvokeMember(String name, BindingFlags invokeAttr, Binder binder, Object target, Object[] args, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParameters) +473
System.Web.Mail.LateBoundAccessHelper.CallMethod(Object obj, String methodName, Object[] args) +58

[HttpException (0x80004005): Could not access 'CDO.Message' object.]
System.Web.Mail.LateBoundAccessHelper.CallMethod(Object obj, String methodName, Object[] args) +111
System.Web.Mail.CdoSysHelper.Send(MailMessage message) +1861
System.Web.Mail.CdoSysHelper.Send(String from, String to, String subject, String messageText) +72
System.Web.Mail.SmtpMail.Send(String from, String to, String subject, String messageText) +167
MyApp.Web.SignUp.submitButton_Click(Object sender, EventArgs e)
System.Web.UI.WebControls.Button.OnClick(EventArgs e) +108
System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument) +57
System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +18
System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +33
System.Web.UI.Page.ProcessRequestMain() +1273

Version Information: Microsoft .NET Framework Version:1.1.4322.2032; ASP.NET Version:1.1.4322.2032

I went through all the possible changes…

  • Recently adding the .Net Framework 2.0 to this machine: After re-registering the Web application to use ASP.Net 1.1, I still received the same error (as seen at the bottom of the error message). No problem here.
  • Bad permissions on IIS’s SMTP service: Authentication was anonymous. Relaying was allowed for the local computer. No other restrictions. Sending an email from SQL using CDO worked fine. No problem here
  • Restart services, reboot computer, bang mouse on desk, slam keys on keyboard: Didn’t help.

A quick Google search brought me to the System.Web.Mail Web site. Of all the ideas given in the site, one of them worked. From that Web site:

I have no idea why this suggestion works, but I found it on the web. I figured I would mention it, just in case Suggestion 1 did not work. Instead of specifying
SmtpMail.SmtpServer = “127.0.0.1”
try
SmtpMail.SmtpServer.Insert( 0, “127.0.0.1 or your mail server name here”)

Yes, it worked for me, too, by changing SmtpMail.SmtpServer = ConfigurationSettings.AppSettings["SmtpServer"] to SmtpMail.SmtpServer.Insert( 0, ConfigurationSettings.AppSettings["SmtpServer"] ).

Don’t ask me why, either…

Where’s my sessions (ASP.Net)?

A former coworker sent me a note asking me if I had any ideas why his ASP.Net sessions weren’t timing out. One thing came to mind: They may be timing out, but his code may be structured in a way that he won’t recognize it. I’ve seen this in the past when people try to take actions in the Session_End event handler using Session object values. The problem is, you can’t access the Session object from the Session_End event, because the Session object for that user no longer exists at the time the event is fired.

There’s a great article on EggGeadCafe: ASP.Net Session State FAQ. Some of the best items in this article are quoted below:

  • “In order for Session_End to be fired, your session state has to exist first. That means you have to store some data in the session state and has completed at least one request.”
  • “Session_End is fired internally by the server, based on an internal timer. Thus, there is no HttpRequest associted when that happens. That is why Response.Redirect or Server.Transferdoes not make sense and will not work.”
  • “Session state is available only after the HttpApplication.AcquireRequestState event is called.”
  • “Q: Will my session state be saved when my page hit an error?
    A: No. Unless you call Server.ClearError in your exception handler.”

Good luck to my friend in solving his problem; if the above tips don’t help him out, I’m sure a subsequent blog entry will have more details.

Rendering content using custom server controls in ASP.Net

I’ve got a few Web sites, but one thing I’m not is a Web designer. I typically find a good thing and stick with it. One of those is what I call content panels.

A content panel (in my world) is an area of content on a Web page that is not part of the page template – that is, it isn’t part of the header, footer, or navigation. It allows you to divide your main content region of a Web page into logical blocks. Sounds like a <DIV> tag, doesn’t it? Well, in many respects, it is. It’s actually a <DIV> tag on steroids.

Consider the following:

This is a plain div tag.
This is a div tag with some style.

A <DIV> tag encloses content, but adds no specific style or functionality on its own. A content panel encloses content and adds a specific style and functionality to it.

For an example of my content panels in action, check out EQ2Craft. Notice how the sections in the main body have similar style, layout, and functionality. They’re all created by the same server control: ContentPanel.

The ContentPanel control supports specific functionality:

  • Select one of three colors as defined in a separate stylesheet.
  • Set the title text, or hide the title bar entirely.
  • Turn the expand/collapse feature on or off.
  • Turn the content frame on or off.

All of these are set using public properties:

  • Title (string): Get or set the title for the content panel.
  • ShowTitle (bool): Get or set a flag which turns the rendering of the title bar on and off.
  • ShowFrame (bool): Get or set a flag which turns the rendering of the frame on and off.
  • Collapseable (bool): Get or set a flag which turns the expand/collapse widget on and off.
  • Color (ContentPanelColor): Get or set the color, based on the ContentPanelColor enum, which allows three values: Primary, Secondary, and Tertiary. Each of these corresponds to styles which must exist in your stylesheet:
    • colorPanelTitle (as in PrimaryPanelTitle, SecondaryPanelTitle, and TertiaryPanelTitle) reflects the style of the title bar.
    • colorPanelContent (as in PrimaryPanelContent, SecondaryPanelContent, and TertiaryPanelContent) reflects the style of the content area.

The code for my generic ContentPanel class follows:

    public enum ContentPanelColor
    {
        Primary,
        Secondary,
        Tertiary
    }

    public class ContentPanel : Control
    {
        private string title;
        private bool showTitle;
        private bool showFrame;
        private bool collapseable;
        private ContentPanelColor color;

        public string Title
        {
            get { return title; }
            set { title = value; }
        }
        public bool ShowTitle
        {
            get { return showTitle; }
            set { showTitle = value; }
        }
        public bool ShowFrame
        {
            get { return showFrame; }
            set { showFrame = value; }
        }
        public bool Collapseable
        {
            get { return collapseable; }
            set { collapseable = value; }
        }
        public ContentPanelColor Color
        {
            get { return color; }
            set { color = value; }
        }

        public ContentPanel()
        {
            title = "";
            showTitle = true;
            showFrame = true;
            collapseable = true;
        }

        protected override void OnLoad(EventArgs e)
        {
            Literal startContentDiv = new Literal();
            Literal endContentDiv = new Literal();

            this.Controls.AddAt( 0, startContentDiv );
            this.Controls.Add( endContentDiv );

            startContentDiv.Text = "<div class=\"ContentPanelBody\" id=\"" + startContentDiv.ClientID + "\">";
            endContentDiv.Text = "</div>";

            if ( showFrame )
            {
                Literal startFrameDiv = new Literal();
                Literal endFrameDiv = new Literal();
                startFrameDiv.Text = "<div class=\"" + color.ToString().ToLower() + "PanelContent\">";
                endFrameDiv.Text = "</div>";
               
                this.Controls.AddAt( 0, startFrameDiv );
                this.Controls.Add( endFrameDiv );
            }

            if ( showTitle )
            {
                HtmlGenericControl titleDiv = new HtmlGenericControl( "div" );
                titleDiv.Attributes["class"] = color.ToString().ToLower() + "PanelTitle";

                HtmlImage ecImg = null;

                if ( collapseable )
                {
                    HtmlGenericControl ecDiv = new HtmlGenericControl( "div" );
                    ecDiv.Attributes["style"] = "float:right;";

                    ecImg = new HtmlImage();
                    ecImg.ID = "";
                    ecImg.Src = "~/Images/Collapseable.gif";
                    ecImg.Alt = "Expand/Collapse";
                    ecImg.Height = 9;
                    ecImg.Width = 9;
                    ecImg.Border = 0;

                    ecDiv.Controls.Add( ecImg );
                    titleDiv.Controls.Add( ecDiv );
                }

                if ( title.Length > 0 )
                {
                    Literal titleText = new Literal();
                    titleText.Text = title;
                    titleDiv.Controls.Add( titleText );
                }

                this.Controls.AddAt( 0, titleDiv );
                if ( ecImg != null )
                    ecImg.Attributes["onclick"] = "expandCollapse( '" + startContentDiv.ClientID + "', '" + ecImg.ClientID + "') ;";
            }

            base.OnLoad (e);
        }
    }

Friendly Session Timeouts, the JavaScript Way

A client recently asked me if there was any way to notify visitors to their Web site that their session was about to expire. Fortunately, there’s an easy way to do this using JavaScript.

First, add the following code block to your standard JavaScript include file. (You are using an included file for your JavaScripts, aren’t you?) Essentially, you want the following piece of JavaScript code to be available on each page:

function ShowTimeoutWarning ()
{
    window.alert( "You will be automatically logged out in five minutes unless you do something!" );
}

That little function does nothing more than show a popup message to the user. The text of the message (and the five minutes) duration is arbitrary; you can make them whatever you want.

How will we invoke the popup? By using a simple JavaScript function called setTimeout. Pass two parameters to this function: the name of another JavaScript function to call, and the amount of time to wait (in milliseconds) before calling that function. I add the following line into the HTML <HEAD> section within a <SCRIPT> tag.

setTimeout( 'ShowTimeoutWarning();', 900000 );

90,000 milliseconds is 15 minutes (15 minutes * 60 seconds per minute * 1000 milliseconds per second), so in 15 minutes the ShowTimeoutWarning() function will be called automatically. Of course, if the user unloads this Web page (by going to another Web page), the timer disappears.

That 15 minute threshold for the timer was based on five minutes less than the 20 minute session timeout in the Web application.

But what if your Web site (like my client’s Web site) had different session timeouts for different users? You can use a little in-line .Net code to dynamically calculate the timeout period. Here’s how I did it for my client — by adding the following lines within a <SCRIPT> tag:

if ( HttpContext.Current.User.IsInRole( "User" ) )
{
    Response.Write( "setTimeout('ShowTimeoutWarning();', " +
        ( ( Session.Timeout - 5 ) * 60000 ).ToString() + " );" );
}

Notice how I calculated the timeout dynamically by using the Session object’s Timeout property. Session.Timeout is stored in minutes, so subtract your announcement time (5 minutes) from that number, then multiply by 60,000 (milliseconds per minute) to get your user-customized timeout alert.