:::: MENU ::::

Thursday, August 27, 2009

Introduction
The .NET Framework is big. Really big. The System.Web assembly, which contains the guts of ASP.NET, is comprised of nearly 2,000 types, over 23,000 methods, and more than 12,500 properties. And that's not counting any of the functionality added to ASP.NET since version 2.0. ASP.NET AJAX, the ListView control, Dynamic Data, URL routing, and other features add hundreds of new types and thousands of new methods and properties.

Given the size and scope of the .NET Framework and ASP.NET even there are certain to be dark corners for even the most experienced developers. There are certain classes, methods, and properties in the .NET Framework that every ASP.NET developer is intimately familiar with: the Request.QueryString collection; the Session object; Page object properties like IsValid and IsPostBack. Yet even in these familiar classes there are very useful and very helpful properties, methods, and features that are less widely known. Heck, I've been building ASP.NET applications and writing about ASP.NET functionality and features full time since 2001, and once or twice a month I still stumble across an unknown feature or a helpful property or method buried in some dark corner of the framework.

This article lists four helpful methods, properties, and features in the .NET Framework that, in my experience, are not widely known to ASP.NET developers. Read on to see my list!


Typically, ASP.NET developers design, implement, and test their web applications from their personal computer, visiting the website through localhost. When the site is ready to be deployed it is moved to a different web server and the visitors are remote, reaching the website from a domain name like www.yoursite.com. Likewise, when developing the application it is not uncommon to have debugging enabled, which entails going to Web.config and setting the <compilation> element's debug attribute to true. However, this setting should always be set to false in production, as Scott Guthrie notes in his blog entry Don't run production ASP.NET Applications with debug="true" enabled.

Sometimes its helpful to know if the user visiting the website is coming from localhost or if debugging is enabled. For example, you might want to display debugging- or development-related information at the top of each web page, such as the time it took to execute the page or detailed information about the currently logged-in user. However, this information should not be shown to end users; rather, it should only be displayed if the user is visiting locally or if debugging is enabled.

The good news is that it's quite easy to determine whether a visitor has arrived at the site locally, thanks to the Response object's IsLocal property. Response.IsLocal returns True if the visitor is coming from localhost, False otherwise. To determine whether debugging is enabled, use the HttpContext object's IsDebuggingEnabled property. The code snippet below shows how to display a Panel (pnlDiagnosticInfo) only if the user is arriving from localhost and debugging is enabled.

// C#
if (Request.IsLocal && HttpContext.Current.IsDebuggingEnabled)
   pnlDiagnosticInfo.Visible = true;


' VB
If Request.IsLocal AndAlso HttpContext.Current.IsDebuggingEnabled Then
   pnlDiagnosticInfo.Visible = True
End If

Note that to use the HttpContext object's properties you need to first use HttpContext.Current to get a reference to the HttpContext object associated with this request.

2. The Panel Web Control's DefaultButton Property
A <form> in a web page can be submitted in a number of ways: a user can click a submit button; JavaScript can submit the form by calling its submit() function; or a user may hit Enter in a textbox. It's that last technique - hitting Enter in a textbox to submit the form - that can cause problems for web pages that contain multiple submit buttons. For example, an ASP.NET web application might have a master page that includes a search TextBox and Button Web control on every page. When the search Button on the master page is clicked, a postback ensues, the Button control's Click event handler runs, and the user is taken to a page that displays the results of their search query.

Now, imagine a content page that uses this master page and contains its own TextBox controls and a Button control titled "Insert." When the Insert Button is clicked, there is a postback and the Button's Click event handler fires, at which point a new record is added to the database, say.

What happens when we visit this content page in a browser, type some text into the search TextBox, and hit Enter? Hitting Enter causes the browser to submit the <form>, but what Button does the browser indicate as having been "clicked?" If the browser says that the Search Button was clicked, then that Button's Click event handler will fire on postback, but if the browser indicates that the Insert Button is clicked then that Button's Click event handler will fire. Typically, when hitting Enter in a TextBox the browser will report that the first submit button in the form has been clicked. The net result is that regardless of whether the focus was in the Search TextBox or in one of the TextBox controls in the content page, the same Button will report as being "clicked" when the user hits the Enter key. What we want is for the browser to report that the Search Button was "clicked" when the user hits Enter in the search TextBox, and for the browser to report that the Insert Button was "clicked" when the user hits Enter from one of the TextBoxes in the content page. (For a more in-depth discussion of this problem, see Two Common Pitfalls When Submitting a Web Form Using the Enter Key.)

This conundrum can be fixed by placing the search TextBox and Button inside a Panel Web control and setting the Panel's DefaultButton property to the ID of the search Button Web control. The master page's markup would look something like:

<asp:Panel runat="server" ID="pnlSearch" DefaultButton="btnSearch" ...>
   Search: <asp:TextBox runat="server" ID="txtSearch" ... />
   <asp:Button runat="server" ID="btnSearch" Text="Search" ... />
</asp:Panel>

Setting the Panel's DefaultButton property causes the Panel to render with additional JavaScript that is triggered whenever the user presses a key when focused within the Panel. The JavaScript function monitors these keystrokes, and when the Enter key is pressed the code submits the <form>, reporting that the associated Button control was clicked.

For more information on this property, check out my blog entry, The DefaultButton Property - News To Me!

3. Creating Fully Qualified URLs Using Request.Url.GetLeftPart(UriPartial.Authority)
The Request object includes a number of properties for determining path information. For example, the Request object's PhysicalPath property returns the physical path to the currently executing page (e.g., D:\Websites\DemoApp\Tutorial01\MyPage.aspx) whereas the Url property returns the fully qualified URL of the currently requested page (e.g., http://www.yourserver.com/Tutorial01/MyPage.aspx). But what if you want to build a fully qualified URL from scratch?

This is a common challenge when writing code that sends an e-mail that includes a URL back to the website. For example, if your site supports user accounts you might want to send newly registered users an e-mail message welcoming them to the site. You might want to include a URL to the login page in this e-mail. If you know the fully qualified URL of the login page - such as http://www.yourserver.com/Login.aspx - you can hard-code it into the e-mail's body. But what if you haven't settled on a domain name yet? Or what if the software you are developing will be used by various clients, each who have their own domain names? In this case you need to generate a fully qualified URL programmatically. You know the file name of the login page (~/Login.aspx), but you need to write code to generate the left-hand portion, http://www.yourserver.com/, or perhaps http://www.yourserver.com/MyApp/, if the application is rooted at a virtual directory.

Rick Strahl shows how this information can be pieced together from various variables in the ServerVariables collection, but there's an even easier way using the Request object's Url property's GetLeftPart method. In short, the GetLeftPart method returns the specified portion of the URL. The UriPartial.Authority enumeration value passed into this method instructs GetLeftPart method to return the protocol (http:// or https://) along with the domain name and port number (if present). For example, if the requested page's URL is http://www.yourserver.com:8080/Tutorial01/MyPage.aspx, then calling Request.Url.GetLeftPart(UriPartial.Authority) returns the string http://www.yourserver.com:8080.

The following code snippet shows how to build up a fully qualified URL for the login page using Request.Url.GetLeftPart(UriPartial.Authority):

// C#
string loginUrl = Request.Url.GetLeftPart(UriPartial.Authority) +
                  Page.ResolveUrl("~/Login.aspx");

' VB
Dim loginUrl As String = Request.Url.GetLeftPart(UriPartial.Authority) & _
                         Page.ResolveUrl("~/Login.aspx")

The call to Page.ResolveUrl("~/Login.aspx") generates a rooted path to the login page, such as /Tutorial01/Login.aspx. The net effect is that loginUrl is assigned the fully qualified path to the login page, http://www.yourserver.com:8080/Tutorial01/Login.aspx.

4. Taking An ASP.NET Web Application Offline
When performing maintenance on a production website, pushing up new or modified code/web pages, or taking any other sort of action that may disrupt a user's experience, it is a good idea to take the application offline and replace it with a single web page that explains that the website is currently unavailable. IIS, Microsoft's production-grade web server, includes an option to redirect all incoming traffic to a specific URL. This includes requests for any sort of content - ASP.NET pages, static HTML pages, images, CSS files, JavaScript files... whatever.

ASP.NET provides its own mechanism for taking an application offline. In short, if you upload a file to your website named App_Offline.htm the ASP.NET engine will automatically display the contents of the App_Offline.htm for any request to an ASP.NET page. By merely uploading such a file you effectively take your site offline and can display a nice-looking, informative message to visitors explaining why the site is offline, when you expect it to be back up, and so on. To bring the site back up, simply delete (or rename) the App_Offline.htm file.

The main benefit of App_Offline.htm over configuring IIS to redirect all traffic to a specified URL is that App_Offline.htm can be used in virtually any environment, whereas configuring IIS requires access and permission to modify the web server's settings. Typically, such access is not granted in a shared hosting environment, making the App_Offline.htm approach more amenable for such situations.

An earlier article on 4Guys, Taking an ASP.NET Application Offline, shows how to configure IIS to redirect all requests to a static URL, examines alternative approaches to taking an ASP.NET application offline, and discusses App_Offline.htm in more detail. It also includes a demo on how to programmatically create the App_Offline.htm file from a web page, thereby allowing a site to be taken offline from the web. (No need to FTP a file!)

Bear in mind that there are some subtleties that you must be aware of when using App_Offline.htm. As noted in Taking an ASP.NET Application Offline:

The ASP.NET runtime will not return an App_Offline.htm file if it exceeds 1 MB in size. Furthermore, if your App_Offline.htm file is too small (less than 512 bytes), Internet Explorer will display its "friendly 404 error page" rather than the content returned by App_Offline.htm (assuming friendly HTTP error pages are enabled in IE, which they are, by default).

Conclusion
Due to its size and scope, the .NET Framework has a lot of pigeonholes where useful functionality is tucked away. Even when working with the .NET Framework on a daily basis for years on end there are still spots that remain unexplored. This article shared four helpful bits of knowledge on ASP.NET that you may not have known about before. Is there a golden nugget or two in the .NET Framework that you use on a regular basis, but have found that your fellow developers are usually unaware of?

Wednesday, August 26, 2009

Refactoring is an integral part of continually improving your code while it moves forward through time. Without refactoring you accrue technical debt, forget what portions of code do and create code that is resistant to any form of testing. It is an easy concept to get started with and opens the door to much better practices such as unit testing, shared code ownership and more reliable, bug-free code in general.
Because of the importance of refactoring, throughout the month of August I will be describing one refactoring a day for the 31 days of August. Before I begin, let me prefix this series with the fact that I am not claiming ownership of the refactorings I will describe, although I will try to bring some additional description and perhaps some discussion around each. I have chosen not to list the full set of refactorings on this first post until I actually post the refactoring. So as the month progresses I will update this post with links to each of the refactorings.
First on the list is credit where it is due. The majority of these refactorings can be found Refactoring.com, some are from Code Complete 2nd Edition and others are refactorings that I find myself doing often and from various other places on the interwebs. I don’t think it’s important to note on each refactoring where it came from, as you can find refactorings of similar names found on various blogs and articles online.
On that note, I will be publishing the first post starting tomorrow that begins the 31 day marathon of various refactorings. I hope you all enjoy and learn something from the refactorings! 
  1. Refactoring Day 1 : Encapsulate Collection
  2. Refactoring Day 2 : Move Method
  3. Refactoring Day 3 : Pull Up Method
  4. Refactoring Day 4 : Push Down Method
  5. Refactoring Day 5 : Pull Up Field
  6. Refactoring Day 6 : Push Down Field
  7. Refactoring Day 7 : Rename (method, class, parameter)
  8. Refactoring Day 8 : Replace Inheritance with Delegation
  9. Refactoring Day 9 : Extract Interface
  10. Refactoring Day 10 : Extract Method
  11. Refactoring Day 11 : Switch to Strategy
  12. Refactoring Day 12 : Break Dependencies
  13. Refactoring Day 13 : Extract Method Object
  14. Refactoring Day 14 : Break Responsibilities
  15. Refactoring Day 15 : Remove Duplication
  16. Refactoring Day 16 : Encapsulate Conditional
  17. Refactoring Day 17 : Extract Superclass
  18. Refactoring Day 18 : Replace exception with conditional
  19. Refactoring Day 19 : Extract Factory Class
  20. Refactoring Day 20 : Extract Subclass
  21. Refactoring Day 21 : Collapse Hierarchy
  22. Refactoring Day 22 : Break Method
  23. Refactoring Day 23 : Introduce Parameter Object
  24. Refactoring Day 24 : Remove Arrowhead Antipattern
  25. Refactoring Day 25 : Introduce Design By Contract Checks
  26. Refactoring Day 26 : Remove Double Negative 
  27. Refactoring Day 27 : Remove God Classes 
  28. Refactoring Day 28 : Rename boolean methods 
  29. Refactoring Day 29 : Remove Middle Man 
  30. Refactoring Day 30 : Return ASAP 
  31. Refactoring Day 31 : Replace Conditional with Polymorphism

    Thursday, August 20, 2009

    There was a question about this on the Asp.Net forums and after a quick search I didn't find a good generic function so I thought I'd supply one.

    Note: I wanted this to be as broad and useful as possible, so the second parameter is a ListControl which both the ListBox and DropDownList inherit from.

    I also made sure the function would handle enums that had non-contiguous values that didn't necessarily start at zero.

    // ---- EnumToListBox ------------------------------------

    //

    // Fills List controls (ListBox, DropDownList) with the text

    // and value of enums

    //

    // Usage:  EnumToListBox(typeof(MyEnum), ListBox1);

     

    static public void EnumToListBox(Type EnumType, ListControl TheListBox)

    {

        Array Values = System.Enum.GetValues(EnumType);

     

        foreach (int Value in Values)

        {

            string Display = Enum.GetName(EnumType, Value);

            ListItem Item = new ListItem(Display, Value.ToString());

            TheListBox.Items.Add(Item);

        }

    }

     

    I tested with an existing enum and a custom enum:

    enum CustomColors

    {

        BLACK = -1,

        RED = 7,

        GREEN = 14,

        UNKNOWN = -13

    }

     

    protected void Button1_Click(object sender, EventArgs e)

    {

        EnumToListBox(typeof(DayOfWeek), DropDownList1);

     

        EnumToListBox(typeof(CustomColors), ListBox1);

    }

    Note: I initially tried to get the order of the items in the ListBox to match the order of the items in the enum but both the Enum.GetValues and Enum.GetNames functions return the items sorted by the value of the enum. So if you want the enums sorted a certain way, the values of the enums must be sorted that way. I think this is reasonable; how the enums are physically sorted in the source code shouldn't necessarily have any meaning.

     

    You could use an Extension Method on a ListControl to create a new method. This would allow you to create code that looks like this:DropDownList.BindToEnum(typeof(enum));

    Doesn't make it any more or less functional, just a bit prettier.

     

     

    Monday, August 10, 2009

    Yesterday evening at work a team member and I were pair programming.  We had a disagreement about how to code a few lines.  The question was around whether to use the ?? operator or to use an if statement using String.IsNullOrEmpty.  We settled it like most developers do and that is with a benchmark.  Here’s the fun we had.

    To give you an idea as to what we were doing here’s some context.  We had a function that took an object.  We wanted to add extra context data to the object, but only if the object didn’t have it set.  In other words, the data may have already been set somewhere else in the application and we didn’t want to override what was already set.  There were two ways to do this and since I was typing I started typing the following.

       1: private void AddContextData(LogEntry entry)
       2: {
       3:     entry.Url = entry.Url ?? HttpContext.Current.Request.Url.ToString();
       4:     entry.UserIpAddress = entry.UserIpAddress ?? HttpContext.Current.Request.UserHostAddress;
       5:     entry.UserName = entry.UserName ?? HttpContext.Current.User.Identity.Name;
       6: }

    My pairing partner started in on me immediately.  He was thinking it should be written like this.

       1: private void AddContextData(LogEntry entry)
       2: {
       3:     if (String.IsNullOrEmpty(entry.Url)) entry.Url = Context.Current.Request.Url.ToString();
       4:     if (String.IsNullOrEmpty(entry.UserIpAddress)) entry.UserIpAddress = HttpContext.Current.Request.UserHostAddress;
       5:     if (String.IsNullOrEmpty(entry.UserName)) = entry.UserName = HttpContext.Current.User.Identity.Name;
       6: }

    The ?? for those that aren’t familiar with it is called the “null coalescing” operator. Scott Gu wrote about it late 2007 if you want some additional information and samples.  It is a fun little operator and can save you a lot of typing in more places than you think.  Most developers though don’t think to use it and instead code the long handed version doing a check using a if block. 

    In the end we decided to settle our disagreement and go with the one that was the fastest.  Thus we whipped up a quick benchmark to test one vs. the other.  Here’s the benchmark code for both samples.

    ?? Operator Benchmark Code

       1: string x = "foo bar";
       2: Stopwatch watch = new Stopwatch();
       3: long ticks = watch.Time(() => x = x ?? "bar foo", 100000);
       4: textBox1.Text += "??: " + ticks.ToString() + Environment.NewLine;

     

    Note: If you copy the above code, it will not work on your machine unless you have a Time() extension method as part of your arsenal. 

    if { } Benchmark Code

       1: string x = "foo bar";
       2: Stopwatch watch = new Stopwatch();
       3: long ticks = watch.Time(() =>
       4:                             {
       5:                                 if (String.IsNullOrEmpty(x))
       6:                                 {
       7:                                     x = "bar";
       8:                                 }
       9:                             }
      10:                 , 100000);
      11: textBox1.Text += "if: " + ticks.ToString() + Environment.NewLine;

    The Results

    The results were not all that exciting. Really it only proved there was wasn’t any difference in speed, if anything giving a very very slight edge to the ?? operator.  At any rate it was a fun side bar to end the evening.  Happy null coalescing!

     

    Monday, August 3, 2009

    Here's one thing that that irritates me to no end - the way IIS and ASP.NET inheritance works down from a root directory into child directories.

    I started a new project today and immediately I was greeted by about 15 configuration errors. Like this:

    Server Error in '/WebStore2008' Application.


    Compilation Error

    Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.

    Compiler Error Message: CS0246: The type or namespace name 'Westwind' could not be found (are you missing a using directive or an assembly reference?)

    Source Error:

     

    Line 30:     <pages>
    Line 31:       <namespaces>
    Line 32:         <add namespace="Westwind.Tools" />
    Line 33:         <add namespace="Westwind.Web.Controls" />
    Line 34:       </namespaces>


    Source File: c:\Westwind\Web.Config    Line: 32

    Show Detailed Compiler Output:

     

    c:\windows\system32\inetsrv> "c:\Windows\Microsoft.NET\Framework\v3.5\csc.exe" /t:library /utf8output /R:"C:\Windows\assembly\GAC_MSIL\System.ServiceModel.Web\3.5.0.0__31bf3856ad364e35\System.ServiceModel.Web.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Xml\2.0.0.0__b77a5c561934e089\System.Xml.dll" /R:"C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Web.Mobile\2.0.0.0__b03f5f7f11d50a3a\System.Web.Mobile.dll" /R:"C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\webstore2008\6c272261\3f6f6ff2\assembly\dl3\ea25c105\1062d9c4_dbdbc701\WebStore2008.DLL" /R:"C:\Windows\assembly\GAC_MSIL\System.Web.Services\2.0.0.0__b03f5f7f11d50a3a\System.Web.Services.dll" /R:"C:\Windows\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Xml.Linq\3.5.0.0__b77a5c561934e089\System.Xml.Linq.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Web.Extensions\3.5.0.0__31bf3856ad364e35\System.Web.Extensions.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Core\3.5.0.0__b77a5c561934e089\System.Core.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Runtime.Serialization\3.0.0.0__b77a5c561934e089\System.Runtime.Serialization.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Configuration\2.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.WorkflowServices\3.5.0.0__31bf3856ad364e35\System.WorkflowServices.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.IdentityModel\3.0.0.0__b77a5c561934e089\System.IdentityModel.dll" /R:"C:\Windows\assembly\GAC_32\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Drawing\2.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.ServiceModel\3.0.0.0__b77a5c561934e089\System.ServiceModel.dll" /R:"C:\Windows\assembly\GAC_32\System.Web\2.0.0.0__b03f5f7f11d50a3a\System.Web.dll" /R:"C:\Windows\assembly\GAC_32\System.EnterpriseServices\2.0.0.0__b03f5f7f11d50a3a\System.EnterpriseServices.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.Data.DataSetExtensions\2.0.0.0__b77a5c561934e089\System.Data.DataSetExtensions.dll" /R:"C:\Windows\assembly\GAC_MSIL\System.DirectoryServices\2.0.0.0__b03f5f7f11d50a3a\System.DirectoryServices.dll" /out:"C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\webstore2008\6c272261\3f6f6ff2\App_Web_default.aspx.cdcab7d2.wfdhi8ei.dll" /debug- /optimize+ /warnaserror /w:4 /nowarn:1659;1699 /warnaserror-  "C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\webstore2008\6c272261\3f6f6ff2\App_Web_default.aspx.cdcab7d2.wfdhi8ei.0.cs" "C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\webstore2008\6c272261\3f6f6ff2\App_Web_default.aspx.cdcab7d2.wfdhi8ei.1.cs"
     
     
    Microsoft (R) Visual C# 2008 Compiler Beta 2 version 3.05.20706.1
     
    for Microsoft (R) .NET Framework version 3.5
    Copyright (C) Microsoft Corporation. All rights reserved.
     
    c:\Westwind\Web.Config(32,11): error CS0246: The type or namespace name 'Westwind' could not be found (are you missing a using directive or an assembly reference?)
    c:\Westwind\Web.Config(33,11): error CS0246: The type or namespace name 'Westwind' could not be found (are you missing a using directive or an assembly reference?)

     

    This is of course only one in a whole string of errors that will come up one at a time. This particular error is due to a BIN deployed component that doesn't exist in my newly created application and the app is expecting the namespace which of course isn't actually there.

    So this is a brand new project why these errors? Well, the problem is that the newly created app automatically inherits the root web's config settings. So if you use any custom modules, handlers, or as above even add a namespace, references that are not used in a new project down the virtual directory you'll get errors because those settings usually don't make sense in the lower virtual directories.

    You can fix this by explicitly overriding adding a clear tag:

        <pages>
          <namespaces>
            <clear />
          </namespaces>
        </pages>

    But this can be a royal pain if you have to do this on 10 or 20 different sections of the configuration for each application downwind of the root virtual. If you have 10 or 20 virtuals this gets old real quick, and it continues to be volatile if any changes are made to web.config in the root later on which can basically bring down all the applications on a server.

    Don't think this is a big problem? Consider that configuration just got a lot more complex with the introduction of IIS 7 into the mix where you now have to worry about mixtures of IIS 7 settings (in system.webserver) in addition to the settings in system.web. Worse consider the slew of 'default' configuration settings that a .NET 3.5 app requires (think of the MS Ajax includes, handlers and modules, plus the 3.5 libraries) and imagine all that gunk in a root application. If you have a 2.0 virtuals underneath the root that things will get real ugly.

    Stop Inheritance cold with <location inheritInChildApplications="false">

    Thankfully there's a not very obvious way to work around this whole issue by using a location tag. Using the inheritInChildApplication attribute in the location element allows you to basically stop all inheritance dead from the current application down. All that's required is that the settings you want to protect are stored inside of a <location> element like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <configSections>
        <section name="webConnectionConfiguration" type="System.Configuration.NameValueSectionHandler,System,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <section name="wwBanner" type="System.Configuration.NameValueSectionHandler,System,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      </configSections>
      <connectionStrings>
        <add name="WestWindAdmin" connectionString="server=.;database=WestWindAdmin;integrated security=true;" providerName="" />
      </connectionStrings>
     
      <location inheritInChildApplications="false">
        <system.web>
          <compilation debug="true">
            <assemblies>
              <add assembly="System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" />
            </assemblies>
          </compilation>
          <pages>
            <namespaces>
              <add namespace="Westwind.Tools" />
              <add namespace="Westwind.Web.Controls" />
            </namespaces>
          </pages>
        </system.web>
        <system.webServer>
          <handlers>
            <add name="wst West Wind Web Connection" path="*.wst" verb="*" modules="IsapiModule" scriptProcessor="c:\westwind\wconnect\wc.dll" resourceType="Unspecified" />
          </handlers>
          <validation validateIntegratedModeConfiguration="false" />
        </system.webServer>
      </location>
      
      <wwBanner>
          <add key="ConnectionString" value="server=.;database=WestwindAdmin;integrated security=true;" />
          <add key="BannerManagerHandlerUrl" value="~/wwBanner.ashx" />
          <add key="TrackStatistics" value="True" />
          <add key="HomeUrl" value="~/admin/" />
        </wwBanner>
    </configuration>

    You can basically wrap the location tag around any entries you don't want to inherit or even the entire web.config below the Configuration tag. You can also leave settings that you might actually want to inherit all the way down outside of the location tag. Thus, in the above the connectionStrings and wwBanner configuration settings are visible to child virtuals.

    Why oh, Why?

    I've run into this issue on a number of occasions and this particular option isn't easy to find. The web.config Intellisense and schema preview doesn't show this attribute so it's easy to miss. In addition, when I have looked for this thing I always forget exactly where it is applied: on the Location tag.

    This solves the problem neatly.

    I really wish that IIS 7 provided a global option in its settings to simply stop inheritance at the root Web level for any entries automatically. Honestly I don't see why you'd ever want inheritance to jump virtual directory boundaries. There's no good reason for the inheritance in the first place not when you have machine level configuration settings. When I set up a virtual directory I want to be insured that I have a clean configuration and not have to worry about config settings higher up the chain.

    But at least this problem has a solution even if it's one that I'll forget about probably a few times more. <s> Maybe writing it up here will help jog my memory...

    More