Take total control over the head tag in ASP.NET

by timvasil 10/22/2008 2:44:00 AM

When you use the tag, such as on a master page, the HtmlHead control does the rendering work. The results are pretty dreadful; spacing is pretty screwy for one thing. But that's something I can live with.

One thing I can't live with, however, is third party components like DevExpress adding bogus (and redundant!) stylesheet links within the head tag! I wanted to take control of that tag to filter out these tags, but found HtmlHead to be a sealed class. I can't tell you how many times I've tried to correct bad behavior through the well-established OO practice of inheritance only to find a "sealed" wall in my way. Personally, I think that keyword needs to go! But, alas, in this case there is a way to get around the constraint.

I didn't realize this until today, but ASP.NET has a mechanism for overriding how a control renders its content, even if that control is sealed. The magic is in browser adapters. I learned all about it here:

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

Form.DefaultButton in Firefox

by timvasil 6/4/2008 11:53:00 AM

Setting the DefaultButton property on a form to an ImageButton, LinkButton, or Button with UseSubmitBehavior=false doesn't work on Firefox because the ASP.NET code hooking the Enter key on form submit looks for a "click" method on the button that Firefox does not support.

If you give your button a name attribute, say name='DefaultButton' for example, then throw the following JavaScript in a file that's included on all pages, it'll work.  The trick is to define the click method on Firefox's behalf.

setTimeout(function() { init(); }, 0);

// Fixup link buttons for FF
function init()
{
    if (document.getElementsByName)
    {
        var aoElements = document.getElementsByName("DefaultButton");
        if (aoElements)
        {
            for (var i = 0; i < aoElements.length; i++)
            {
                if (aoElements[i].click == undefined)
                {
                    aoElements[i].click = aoElements[i].onclick;
                }
            }
        }
    }
}


Update on 6/9/08:

That fix apparently doesn't work completely, as when you hit <Enter> in a textarea the form is submitted.  That's a critical flaw.  I didn't want to wholesale replace ASP.NET's WebForm_FireDefaultButton method, but, c'mon, there's only so much tolerance anyone can have for hunting down and working around other people's bugs. 

So, here's a tweaked version of the function that fixes default button handling and works across browsers.  In order to use this patched function, you need to define it *after* all other JavaScript files and also after <ajaxToolkit:ToolkitScriptManager /> if you happen to be using it.

var __defaultFired;
function WebForm_FireDefaultButton(event, target) {
    var element = event.target || event.srcElement;
    if (!__defaultFired && event.keyCode == 13 && !(element && (element.tagName.toLowerCase() == "textarea"))) {
        var defaultButton;
        if (__nonMSDOMBrowser)
            defaultButton = document.getElementById(target);
        else
            defaultButton = document.all[target];

        if (typeof(defaultButton.click) != "undefined") {
            __defaultFired = true;
            defaultButton.click();
            event.cancelBubble = true;
           
            if (event.stopPropagation) event.stopPropagation();
            return false;
        }
        if (typeof(defaultButton.onclick) != "undefined") {
            __defaultFired = true;
            defaultButton.onclick();
            event.cancelBubble = true;
           
            if (event.stopPropagation) event.stopPropagation();
            return false;
        }

    }
    return true;
}

 

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET | JavaScript | Firefox

Handling multiple simultaneous requests from a user in ASP.NET

by timvasil 4/16/2008 8:18:00 PM

If you have an ASP.NET application, have you noticed that out-of-the-box your .aspx requests are serialized?  That is to say, if you request page A.aspx and navigate to B.aspx before A.aspx finishes loading, the B.aspx won't begin until A.aspx completes.

Why is this, you may ask?

The answer is session locking.  The ASP.NET framework protects you from simultaneous access to session state by two HTTP request handing threads by blocking subsequent requests until the first one relinquishes a lock.  This is the default behavior, but it can be overriden (sort of) by using the enableSessionState page attribute:

  • enableSessionState="true" is the default behavior, and acquires a writer lock on session data.  No other readers or writers can gain access while this lock is held.
  • enableSessionState="ReadOnly" acquires a reader lock on session data.  Multiple readers are allowed simultaneously, but no writer can gain access if any reader holds a lock.  Unfortunately, any changes you make to session are local to the request and not visible to other requests that look at the session object.  No error is thrown if you try to modify a "read only" session, so this behavior is not obvious at first blush.
  • enableSessionState="false" doesn't acquire any lock.  The HttpContext.Session property ends up being null, and your page will have no access to session data.

This is a sad state of affairs.  If you want two user requests to execute in parallel, where both need read/write access to the session, you're out of luck.  I wish the ASP.NET framework provided a way for me to manage synchronization to the session data myself as I do in the Java servlet/JSP world, perhaps by using something like a Session.SyncRoot property, but sadly there is absolutely no way to do this. I've looked at the framework code, and in ASP.NET 2.0 there are tons of assumptions all over the place about exclusive write access to the session.

What are the alternative solutions? 

I can think of two solutions off the top of my head:

  1. Create a static singleton (accessible by all sessions) that maps session IDs to string-to-object dictionaries, i.e. IDictionary<string, IDictionary<string, object>>.  The static singleton needs to carefully synchronize access to the data structure.
  2. Create a custom session data store that avoids locking.

Option 1 feels like a hack, as it bypasses all of the built-in session infrastructure and has an obvious drawback of not being automatically cleaned up when sessions terminate.  Calling a cleanup method from a Session_End event is possible, but certainly not elegant.   Here's an implementation:

    /// <summary>
    /// Stores session-specific values in application state so the values are available
    /// immediately, even on pages that do not have access to the session object (e.g. enableSessionState=false).
    /// <para/>
    /// If you use this class, make sure <c>RemoveAllItems</c> is called when sessions end
    /// to ensure memory is reclaimed.
    /// <para/>
    /// All methods on this class are thread-safe.
    /// </summary>
    public static class ApplicationSessionStateStore
    {
        private static bool _enabled;

        /// <summary>
        /// Enables this class for use.  This is a safety check.  By invoking this method,
        /// the caller commits to calling <c>RemoveAllItems</c> when sessions terminate.
        /// </summary>
        public static bool IsEnabled
        {
            get { return _enabled; }
            set { _enabled = value; }
        }

        public static void AssertEnabled()
        {
            if (!_enabled)
            {
                throw new InvalidOperationException("Use of ApplicationSessionStateStore is not enabled.  See 'IsEnabled' property documentation for proper usage.");
            }
        }

        public static T GetItem<T>(string sessionId, string key) where T : class
        {
            return GetItem<T>(sessionId, key, false);
        }

        public static T GetItem<T>(string key) where T : class
        {
            return GetItem<T>(HttpContext.Current.Session.SessionID, key, false);
        }

        public static T GetAndRemoveItem<T>(string sessionId, string key) where T : class
        {
            return GetItem<T>(sessionId, key, true);
        }

        public static T GetAndRemoveItem<T>(string key) where T : class
        {
            return GetItem<T>(HttpContext.Current.Session.SessionID, key, true);
        }

        public static void SetItem(string key, object value)
        {
            SetItem(HttpContext.Current.Session.SessionID, key, value);
        }

        public static void SetItem(string sessionId, string key, object value)
        {
            AssertEnabled();
            HttpContext context = HttpContext.Current;
            HttpApplicationState appState = context.Application;
            IDictionary<string, object> sessionDic;
           
            appState.Lock();
            try
            {
                sessionDic = (IDictionary<string, object>)appState.Get(sessionId);
                if (sessionDic == null)
                {
                    sessionDic = new Dictionary<string, object>();
                    appState.Set(sessionId, sessionDic);
                }
            }
            finally
            {
                appState.UnLock();
            }

            lock (sessionDic)
            {
                sessionDic[key] = value;
            }
        }

        public static void RemoveAllItems()
        {
            RemoveAllItems(HttpContext.Current.Session.SessionID);
        }

        public static void RemoveAllItems(string sessionId)
        {
            HttpContext.Current.Application.Remove(sessionId);
        }

        private static T GetItem<T>(string sessionId, string key, bool removeItem) where T : class
        {
            AssertEnabled();
            HttpContext context = HttpContext.Current;
            HttpApplicationState appState = context.Application;
            IDictionary<string, object> sessionDic = (IDictionary<string, object>)appState.Get(sessionId);

            if (sessionDic == null)
            {
                return null;
            }
            else
            {
                object value;
                lock (sessionDic)
                {
                    if (sessionDic.TryGetValue(key, out value))
                    {
                        if (removeItem)
                        {
                            sessionDic.Remove(key);
                        }
                    }
                }
                return value as T;
            }
        }
    }

This implementation requires some special handing in global.asax:

        protected void Application_Start(Object sender, EventArgs e)
        {
            ApplicationSessionStateStore.IsEnabled = true;
        }

        protected void Session_End(Object sender, EventArgs e)
        {
            ApplicationSessionStateStore.RemoveAllItems();
        }

Option 2 is actually surprisingly complex.  When you implement your own session store, it's not like you're subclassing HttpSession.  Oh no, that would be too simple.  You need to subclass a "store" that provides "store data" that HttpSession encapsulates.  So the custom store must avoid locking requests in the places the native store engages in its reader/writer lock magic, and it must also return a "store data" object whose instance methods are thread-safe.  To further complicate matters, the native implementation of the in-process session store is internal and cannot be directly subclassed by your code, so you have to use reflection to instantiate it and the decorator pattern to subclass it.  Here are all of the steps:

  1. Define a MultiThreadedSesionStateStore that subclasses SessionStateStoreProviderBase.
  2. In the constructor, instantiate the native in-proc store System.Web.SessionState.InProcSessionStateStore using Activator.CreateInstance.
  3. In all the methods that must be defined, invoke the appropriate method on the native store using reflection to find the appropriate MethodInfo and calling Invoke.  The key insight is to call the native store's GetItem method when the multi-threaded store's GetItemExclusive method is called.  In essense, you're faking the native store to treat all session acquisitions as read-only acquisitions so the exclusive writer lock is never held.  Since the session store data object returned is not a copy (the framework makes a copy of read-only session data elsewhere) it's possible to modify the data that should be treated as read-only.
  4. In all methods that return SessionStateStoreData, wrap the store data using a custom subclass of SessionStateStoreData.  In the subclass, wrap the inner SessionStateStoreData.Items collection in a custom ISessionStateItemCollection.
  5. Implement the custom ISessionStateItemCollection class so that all methods lock the wrapped ISessionStateItemCollection's SyncRoot object for reads and writes.  Importantly, when defining the Keys property return a custom NameObjectCollectionBase.KeysCollection instance that is a snapshot of the keys at that point in time.  You'll need to use reflection to create a new instance of this class, as its construtor is internal.  This involves defining a subclass NameValueCollectionBase.
  6. In all methods that accept a SessionStateStoreData object, unwrap the decorator before passing the object to the wrapped session store implementation.

If you don't go through with steps 4 - 6, the session store data object will not be thread safe, and concurrent user requests may end up corrupting this object.  With all steps implemented, reading and writing data to the session, e.g. Session[key] = value, becomes thread-safe.  One important word of caution, though:  If you're placing objects in session state, you must perform locking yourself or ensure the instance methods and properties of the shared object are also thread-safe.

If the 6 steps of this approach seem a bit daunting (I deduced all of this by using the .NET reflector and verifying through inspection that all ways in which the session data could be accessed were done in a thread-safe way), it's also possible to write a session store implementation completely from scratch, but that means you'd have to code up the session timeout logic too.

Why do you need to support concurrent user requests anyway?

There are several reasons why you won't want to run user requests serially.  If you want to allow users to quickly navigate through your app, even when database queries may slow things down a bit, for example, allowing concurrent requests works wonders.  It's also fantastic when multiple dynamic requests may occur in parallel on a page, such as the generation of dynamic charts.

The approach works best in code that periodically checks to ensrue a request is still active, since if a user is bouncing around the app you don't want to spend too much time executing queries and rendering HTML that will never see the light of day.  It's as simple as:

if (!context.Response.IsClientConnected)
{
    context.Response.End();
}

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET | Multithreading

Nested master page bug

by timvasil 12/6/2007 7:43:00 PM

Well, I stumbled upon a bug with nested master pages in .NET 2.0.

 Apparently it's a very bad idea to declare your own method called Master as follows:

        public new MyClass Master
        {
            get { return (MyClass)base.Master; }
        }

This compiles, of course, but any page that uses this master does not get its content inserted into the content placeholders.  Booo.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

Setting a property based on a custom control's inner content

by timvasil 12/3/2007 11:38:00 AM

I want to define a custom ASP.NET control whose default property, say Text, gets set to the inner content the tag:

<prefix:MyTag runat="server" id="myTag">

    Inner content

</prefix:MyTag>

makes myTag.Text == "Inner content" true.

There are several approaches:

  • Override AddParsedSubObject and extract the text from added LiteralControls, throwing exceptions for other conditions, or
  • Override Render to use the Text getter, and implement the Text getter to render the contained controls into a StringWriter wrapped in an HtmlTextWriter.
  • Use special attributes:

    [ParseChildren(true, "Text")]
    [PersistChildren(false)]
    public class MyTag : Control
    {
        private string _text

        public ScriptVariableDefinition()
        {
        }

        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public string Text
        {
            get { return _text}
            set { _text= value; }
        }
    }

 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

Serving embedded resources with WebResource.axd

by timvasil 11/26/2007 1:56:00 PM

If you need to build a URL to an embedded resource and don't have a reference to the Page object to call ClientScript.GetWebResourceUrl, you can use the following:

private static readonly MethodInfo _getWebResourceUrlMethod =
    typeof(ClientScriptManager).GetMethod(
        "GetWebResourceUrl",
        BindingFlags.NonPublic | BindingFlags.Static,
        null,
        new Type[] { typeof(Page), typeof(Type), typeof(string), typeof(bool) }, null);

        /// <summary>
        /// Provides a URL to an embedded resource.
        /// The resource is served with .NET's built in WebResource.axd handler.
        /// You <b>must</b> supply an <c>[assembly: WebResource(resourcePath, mimeType)]
        /// attribute for each resource to be served in this manner.</c>
        /// </summary>
        /// <param name="url">Location of the resource, e.g.
        /// <c>assembly://Assembly.Name/Assembly.Name.Folder1.Folder2/Resource.txt</c>.
        /// The hostname portion specifies the assemby name, and the path specifies the
        /// namespace name and resource name of the resource.</param>
        /// <returns>The external URL to access the embedded resource</returns>
        public static string GetEmbeddedResourceUrl(Uri url)
        {
            if (url == null)
            {
                throw new ArgumentNullException();
            }
            if (url.Scheme != "assembly")
            {
                throw new ArgumentException("Supplied URI must use 'assembly' scheme");
            }
            string assemblyName = url.Host;
            string resourcePath = url.AbsolutePath.Substring(1).Replace('/', '.');
            Assembly resourceAssembly = Assembly.Load(assemblyName);
            return (string)_getWebResourceUrlMethod.Invoke(null,
                   new object[] { null, resourceAssembly.GetTypes()[0], resourcePath, false });
        }

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET | .NET Framework

Attributes and nested properties for custom control designers

by timvasil 11/16/2007 2:26:00 PM

Here's a summary of the types of properties you can specify on custom controls that work well with IntelliSense and validation:

  • Attributes: 
     
    • Simple datatypes (strings, enums, etc.)
      <tag runat="server" simpleProp="simpleValue"/>
       
    • Any datatype with a [TypeConverter] that can convert 1) from a string to the desired type, and 2) from the type to InstanceDescriptor. 
      <tag runat="server" convertibleProp="convertibleValue"/>
         
  • Nested tags when the [ParseChildren(false)] attribute is on the class or no [ParseChildren] attribute exsits:
     
    • Any literal text or control; all content is addded to the custom control's Control collection
      <tag runat="server">
          Literal and <div runat="server">nested</div> content
      </tag>

        
  • Nested tags when the [ParseChildren(true)] attribute is on the class and [PersistenceMode(PersistenceMode.Attribute)] is not specified on the property:
     
    • Simple datatypes and any datatype with a [TypeConverter], as above
      <tag runat="server">
          <simpleProp>simpleValue</simpleProp>
      </tag>
       
       
    • Inner objects and controls with properties (Note:  the getter must construct the object, which unfortunately makes it impossible to specify a subclass in its place within the .aspx markup; null can't be returned)
      <tag runat="server">
          <innerObjProp innerProp="innerVal" innerProp2="innerVal2" />
      </tag>
       
    • Lists of controls when a List or List<T> getter is exposed (Note:  an IList<T> getter doesn't work and you can't return null from the getter; the setter is never called)
      <tag runat="server">
          <listProp>simpleValue</listProp>
      </tag>
       
       
    • A template containing 0+ controls via an ITemplate property; you need to instantiate the template in CreateChildControls() using template.InstantiateIn()
      <tag runat="server">
          <templateProp>Literal and <div runat="server">nested</div> content</templateProp>
      </tag>

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

Fix for "'asp' is an unrecognized tag prefix"

by timvasil 11/15/2007 1:14:00 PM

Recently, while using the beta 2 version of Visual Studio 2008, IntelliSense stopped working when editing .aspx pages and master pages.  Well, it was more than just IntelliSense not working.  I was getting horrible validation errors, such as 'asp' is an unrecocognized tag prefix or device filter.  Considering 'asp' is one of those tag prefixes that should always be defined (thank you, machine.config), I knew something was horribly wrong.  Sources on the web had me try all sorts of things:

  • Make sure the MasterPage is open,
  • Close VS and delete all files in C:\Documents and Settings\{username}\Application Data\Microsoft\VisualStudio\9.0\ReflectedSchemas, and
  • Ensure the file is rooted in an <html> or <body> tag.

No, no, no.  None of that worked.

So then I tried closing the solution, deleting its .suo file (just a work file that can be regenerated), and deleting all files in the obj and bin directories.  And then, like magic, IntelliSense and validation started working again!

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET | Visual Studio

Stupid ASP.NET designer: "could not be set on property" error message

by timvasil 11/5/2007 12:24:00 PM

I hardly ever use the designer, but when creating custom controls I like to do a cursory check in the design view to make sure nothing barfs too badly.

In the process of creating a menu control, everything worked great in the HTML view and at runtime, but in the designer I was puzzled to see this error:

The error is confusing since 'Menu' is a property of test:MenuControl.  Why would the designer be trying to set a property of the control to a reference to the control? 

Solution:  As it turns out, the problem stems from the fact that the control collection I was using as the 'Menu' property had a setter. That's it. As soon as I took out the setter, the designer was happy.

Stupid designer.

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

Manually changing the ID attribute of a control

by timvasil 11/2/2007 1:55:00 PM

The ID of a control, such as a <div runat='server' id='root'>, can be specified directly in the .aspx file.  But what if the ID needs to change dynamically?  For example, wth the YUI tookit you use the ID attribute of the root div to specify the page template, and you may want the template to change based on a user's preference.

In the code behind file, you may try changing the ID by writing something like root.ID = "doc3".  This may work, but if your control is in a naming container (such as a MasterPage) the control will render more like <div id='ctrl00$doc3'>.  There is a way to take complete control of the attribute, however:

   root.ID = null;
   root.Attributes[
"id"] = "doc3";

If you don't set the ID property to null then two ID attributes are rendered:  not what you want, and not valid HTML.

 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET

 

About the author

Tim Vasil Tim Vasil
I'm a software engineer living in Cambridge, MA.

E-mail me Send mail

Search

Calendar

<<  September 2010  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar

Recent comments