Home » Blog » Robin Curry – Confessions of a Software Gym Rat

Robin Curry – Confessions of a Software Gym Rat

Rails framework for ASP.NET

Awhile back I suggested that perhaps we ought to try to bring a Rails-like framework to ASP.NET. Brendan Tompkins lets us know that someone else is doing exactly that with the MonoRail project.

MonoRail (former Castle on Rails) is a MVC web framework inspired on Action Pack. The Action Pack way of development is extremely productive, very intuitive and easily testable.

MonoRail differs from the standard WebForms way of development as it enforces separation of concerns; controllers just handle application flow, models represent the data, and the view is just concerned about presentation logic. Consequently, you write less code and end up with a more maintainable application.

From the website, looks like MonoRail is currently in Beta 5, whatever that means. I’ll have to check this out…

Pardon the Interruption – Testing Code Formatting Issues With Bloglines

I’m having quite a few issues with my code formatting getting trashed by bloglines, so I’m testing using several different options from the CopySourceAsHtml Add-in for Visual Studio 2005, copying and pasting into and out of Word, and using online converters such as CodeHTMLer, and Manoli.net. Unless this kind of stuff fascinates you, you can safely ignore. (If anybody is fascinated and has suggestions, I’m open).

Test 1: CopySourceAsHtml
Options:
Wrap Words: Y
Strip Line Breaks: N

 using System;

    2 using System.Data;

    3 using System.Configuration;

    4 using System.Web;

    5 using System.Web.Security;

    6 using System.Web.UI;

    7 using System.Web.UI.WebControls;

    8 using System.Web.UI.WebControls.WebParts;

    9 using System.Web.UI.HtmlControls;

   10 using System.Web.Configuration;

   11 

   12 /// <summary>

   13 /// Switches to a runtime master page defined either via

   14 /// the web.config or via a page-level property for applicable pages.

   15 /// </summary>

   16 public class DefaultMasterPageModule : IHttpModule

   17 {

   18     public DefaultMasterPageModule()

   19     {

   20     }

   21 

   22 

   23     public void Dispose()

   24     {

   25     }

   26 

   27 

   28     public void Init(HttpApplication context)

   29     {

   30         context.PreRequestHandlerExecute

   31             += new EventHandler(context_PreRequestHandlerExecute);

   32     }

   33 

   34 

   35     private void context_PreRequestHandlerExecute(object sender, EventArgs e)

   36     {

   37         HttpApplication application = sender as HttpApplication;

   38         if (application == null) return;

   39 

   40         System.Web.HttpContext context = application.Context;

   41         if (context == null) return;

   42 

   43         System.Web.UI.Page p = context.Handler as System.Web.UI.Page;

   44         if (p == null) return;

   45 

   46         p.PreInit += new EventHandler(Page_PreInit);

   47     }

   48 

   49 

   50     /// <summary>

   51     ///  Sets the MasterPageFile property for applicable

   52     ///  pages at runtime.

   53     /// </summary>

   54     /// <param name="sender"></param>

   55     /// <param name="e"></param>

   56     void Page_PreInit(object sender, EventArgs e)

   57     {

   58         Page page = sender as Page;

   59         if (page == null) return;

   60 

   61         if (page is ISupportRuntimeMasterSwitching)

   62         {

   63             string runtimeMasterFile =

   64                 ((ISupportRuntimeMasterSwitching)page).RuntimeMasterPageFile;

   65 

   66             if (!string.IsNullOrEmpty(runtimeMasterFile))

   67             {

   68                 page.MasterPageFile = runtimeMasterFile;

   69                 return;

   70             }

   71         }

   72 

   73         if (string.IsNullOrEmpty(page.MasterPageFile)

   74             && MasterCanBeApplied(page))

   75         {

   76             PagesSection pages =

   77                 (PagesSection)ConfigurationManager.GetSection("system.web/pages");

   78 

   79             if (pages.MasterPageFile != string.Empty)

   80             {

   81                 page.MasterPageFile = pages.MasterPageFile;

   82             }

   83         }

   84     }

   85 

   86 

   87     /// <summary>

   88     ///  Attempts to determine whether the page is a content

   89     ///  page (i.e. expecting a master page template) or not.

   90     /// </summary>

   91     /// <param name="page"></param>

   92     /// <returns></returns>

   93     private bool MasterCanBeApplied(Page page)

   94     {

   95         if (page.HasControls())

   96         {

   97             if (page.Controls.IsReadOnly) return false;

   98 

   99             foreach (Control control in page.Controls)

  100             {

  101                 LiteralControl literal = control as LiteralControl;

  102                 if (literal == null ||

  103                     FirstNonWhiteSpaceIndex(literal.Text) >= 0)

  104                 {

  105                     return false;

  106                 }

  107             }

  108             return true;

  109         }

  110         return false;

  111     }

  112 

  113 

  114     private static int FirstNonWhiteSpaceIndex(string s)

  115     {

  116         for (int i=0; i < s.Length; i++)

  117         {

  118             if (!char.IsWhiteSpace(s[i]))

  119             {

  120                 return i;

  121             }

  122         }

  123         return -1;

  124     }

  125 }

Test 2: CopySourceAsHtml
Options:
Wrap Words: N
Strip Line Breaks: N

using System;
    2 using System.Data;
    3 using System.Configuration;
    4 using System.Web;
    5 using System.Web.Security;
    6 using System.Web.UI;
    7 using System.Web.UI.WebControls;
    8 using System.Web.UI.WebControls.WebParts;
    9 using System.Web.UI.HtmlControls;
   10 using System.Web.Configuration;
   11 
   12 /// <summary>
   13 /// Switches to a runtime master page defined either via
   14 /// the web.config or via a page-level property for applicable pages.
   15 /// </summary>
   16 public class DefaultMasterPageModule : IHttpModule
   17 {
   18     public DefaultMasterPageModule()
   19     {
   20     }
   21 
   22 
   23     public void Dispose()
   24     {
   25     }
   26 
   27 
   28     public void Init(HttpApplication context)
   29     {
   30         context.PreRequestHandlerExecute 
   31             += new EventHandler(context_PreRequestHandlerExecute);
   32     }
   33 
   34 
   35     private void context_PreRequestHandlerExecute(object sender, EventArgs e)
   36     {
   37         HttpApplication application = sender as HttpApplication;
   38         if (application == null) return;
   39 
   40         System.Web.HttpContext context = application.Context;
   41         if (context == null) return;
   42 
   43         System.Web.UI.Page p = context.Handler as System.Web.UI.Page;
   44         if (p == null) return;
   45 
   46         p.PreInit += new EventHandler(Page_PreInit);
   47     }
   48 
   49 
   50     /// <summary>
   51     ///  Sets the MasterPageFile property for applicable
   52     ///  pages at runtime.
   53     /// </summary>
   54     /// <param name="sender"></param>
   55     /// <param name="e"></param>
   56     void Page_PreInit(object sender, EventArgs e)
   57     {
   58         Page page = sender as Page;
   59         if (page == null) return;
   60 
   61         if (page is ISupportRuntimeMasterSwitching)
   62         {
   63             string runtimeMasterFile = 
   64                 ((ISupportRuntimeMasterSwitching)page).RuntimeMasterPageFile;
   65 
   66             if (!string.IsNullOrEmpty(runtimeMasterFile))
   67             {
   68                 page.MasterPageFile = runtimeMasterFile;
   69                 return;
   70             }
   71         }
   72 
   73         if (string.IsNullOrEmpty(page.MasterPageFile)
   74             && MasterCanBeApplied(page))
   75         {
   76             PagesSection pages = 
   77                 (PagesSection)ConfigurationManager.GetSection("system.web/pages");
   78 
   79             if (pages.MasterPageFile != string.Empty)
   80             {
   81                 page.MasterPageFile = pages.MasterPageFile;
   82             }
   83         }
   84     }
   85 
   86 
   87     /// <summary>
   88     ///  Attempts to determine whether the page is a content
   89     ///  page (i.e. expecting a master page template) or not.
   90     /// </summary>
   91     /// <param name="page"></param>
   92     /// <returns></returns>
   93     private bool MasterCanBeApplied(Page page)
   94     {
   95         if (page.HasControls())
   96         {
   97             if (page.Controls.IsReadOnly) return false;
   98 
   99             foreach (Control control in page.Controls)
  100             {
  101                 LiteralControl literal = control as LiteralControl;
  102                 if (literal == null ||
  103                     FirstNonWhiteSpaceIndex(literal.Text) >= 0)
  104                 {
  105                     return false;
  106                 }
  107             }
  108             return true;
  109         }
  110         return false;
  111     }
  112 
  113 
  114     private static int FirstNonWhiteSpaceIndex(string s)
  115     {
  116         for (int i=0; i < s.Length; i++)
  117         {
  118             if (!char.IsWhiteSpace(s[i]))
  119             {
  120                 return i;
  121             }
  122         }
  123         return -1;
  124     }
  125 }

Test 3: CopySourceAsHtml
Options:
Wrap Words: N
Strip Line Breaks: Y

using System;
    2 using System.Data;
    3 using System.Configuration;
    4 using System.Web;
    5 using System.Web.Security;
    6 using System.Web.UI;
    7 using System.Web.UI.WebControls;
    8 using System.Web.UI.WebControls.WebParts;
    9 using System.Web.UI.HtmlControls;
   10 using System.Web.Configuration;
   11 
   12 /// <summary>
   13 /// Switches to a runtime master page defined either via
   14 /// the web.config or via a page-level property for applicable pages.
   15 /// </summary>
   16 public class DefaultMasterPageModule : IHttpModule
   17 {
   18     public DefaultMasterPageModule()
   19     {
   20     }
   21 
   22 
   23     public void Dispose()
   24     {
   25     }
   26 
   27 
   28     public void Init(HttpApplication context)
   29     {
   30         context.PreRequestHandlerExecute 
   31             += new EventHandler(context_PreRequestHandlerExecute);
   32     }
   33 
   34 
   35     private void context_PreRequestHandlerExecute(object sender, EventArgs e)
   36     {
   37         HttpApplication application = sender as HttpApplication;
   38         if (application == null) return;
   39 
   40         System.Web.HttpContext context = application.Context;
   41         if (context == null) return;
   42 
   43         System.Web.UI.Page p = context.Handler as System.Web.UI.Page;
   44         if (p == null) return;
   45 
   46         p.PreInit += new EventHandler(Page_PreInit);
   47     }
   48 
   49 
   50     /// <summary>
   51     ///  Sets the MasterPageFile property for applicable
   52     ///  pages at runtime.
   53     /// </summary>
   54     /// <param name="sender"></param>
   55     /// <param name="e"></param>
   56     void Page_PreInit(object sender, EventArgs e)
   57     {
   58         Page page = sender as Page;
   59         if (page == null) return;
   60 
   61         if (page is ISupportRuntimeMasterSwitching)
   62         {
   63             string runtimeMasterFile = 
   64                 ((ISupportRuntimeMasterSwitching)page).RuntimeMasterPageFile;
   65 
   66             if (!string.IsNullOrEmpty(runtimeMasterFile))
   67             {
   68                 page.MasterPageFile = runtimeMasterFile;
   69                 return;
   70             }
   71         }
   72 
   73         if (string.IsNullOrEmpty(page.MasterPageFile)
   74             && MasterCanBeApplied(page))
   75         {
   76             PagesSection pages = 
   77                 (PagesSection)ConfigurationManager.GetSection("system.web/pages");
   78 
   79             if (pages.MasterPageFile != string.Empty)
   80             {
   81                 page.MasterPageFile = pages.MasterPageFile;
   82             }
   83         }
   84     }
   85 
   86 
   87     /// <summary>
   88     ///  Attempts to determine whether the page is a content
   89     ///  page (i.e. expecting a master page template) or not.
   90     /// </summary>
   91     /// <param name="page"></param>
   92     /// <returns></returns>
   93     private bool MasterCanBeApplied(Page page)
   94     {
   95         if (page.HasControls())
   96         {
   97             if (page.Controls.IsReadOnly) return false;
   98 
   99             foreach (Control control in page.Controls)
  100             {
  101                 LiteralControl literal = control as LiteralControl;
  102                 if (literal == null ||
  103                     FirstNonWhiteSpaceIndex(literal.Text) >= 0)
  104                 {
  105                     return false;
  106                 }
  107             }
  108             return true;
  109         }
  110         return false;
  111     }
  112 
  113 
  114     private static int FirstNonWhiteSpaceIndex(string s)
  115     {
  116         for (int i=0; i < s.Length; i++)
  117         {
  118             if (!char.IsWhiteSpace(s[i]))
  119             {
  120                 return i;
  121             }
  122         }
  123         return -1;
  124     }
  125 }

ASP.NET 2.0: Fixing Problems With Nested Master Pages and Config Declared Master Pages at Design Time

Ok, so I’m digging the new Master Page concept in ASP.NET 2.0 and was really looking forward to the designer support since the solution I cooked up in 1.1 had the major disadvantage of frequently making the design window unusable.

I really liked the idea of creating a base master page template and then being able to create nested templates from that for different sections of a website or for different applications within the same larger intranet/internet.

I also was pleased with the ability to set the master page via the web.config file and not have to tie each page explicitly to it, giving me lots of flexibility to swap in and out different templates if I had apps that were applicable to multiple clients.

As you can imagine, I was quite disappointed to learn that I couldn’t have my cake and eat it too. Choosing to do either of these things (nested.master and/or define in web.config) caused the design window to stubbornly refuse me service – despite me getting on my knees and begging.

Ok, so back to the drawing board…

Then I came across this post by Scott Guthrie that showed a hack for tricking the design time window into working with a test or default master page and then switching to the real master page (which may or may not be a nested master) at runtime.

This worked great for solving the issue with nested masters specifically, but unfortunately now required me to tie each page to a runtime master page file explicitly – which I wanted to avoid if possible.

So, I came up with some modifications to Scott’s hack that give me the best of both worlds – I can have the master page populated from the pages config entry at runtime, or it can use the RuntimeMasterPageFile property for individual page-specified masters, all the while working at design-time by setting MasterPageFile=”” and letting the IDE imply the template.

I also moved the solution from a base page – as Scott’s solution shows – to an HttpModule, as I’m of the opinion that Base Pages are an anti-pattern that are too frequently used to solve framework related issues rather than solve object oriented problems.

So, here’s the code below. I’ll mention up front that I’m still a bit unsure about the way I am attempting to determine whether or not a page is a content page and can/should have a master page applied or not. The method I am using is a result of some spelunking into the .NET Framework via Reflector, and I think I’m doing it in a similar fashion as the framework – and seems to work in the limited testing that I’ve done so far, but if anybody has any alternative suggestions or additions, let me know…DefaultMasterPageModule.cs

using System;
    2 using System.Data;
    3 using System.Configuration;
    4 using System.Web;
    5 using System.Web.Security;
    6 using System.Web.UI;
    7 using System.Web.UI.WebControls;
    8 using System.Web.UI.WebControls.WebParts;
    9 using System.Web.UI.HtmlControls;
   10 using System.Web.Configuration;
   11 
   12 /// <summary>
   13 /// Switches to a runtime master page defined either via the web.config or via a page-level property for applicable pages.
   14 /// </summary>
   15 public class DefaultMasterPageModule : IHttpModule
   16 {
   17     public DefaultMasterPageModule()
   18     {
   19     }
   20 
   21 
   22     public void Dispose()
   23     {
   24     }
   25 
   26 
   27     public void Init(HttpApplication context)
   28     {
   29         context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
   30     }
   31 
   32 
   33     private void context_PreRequestHandlerExecute(object sender, EventArgs e)
   34     {
   35         HttpApplication application = sender as HttpApplication;
   36         if (application == null) return;
   37 
   38         System.Web.HttpContext context = application.Context;
   39         if ( context == null ) return;
   40 
   41         System.Web.UI.Page p = context.Handler as System.Web.UI.Page;
   42         if (p == null) return;
   43 
   44         p.PreInit += new EventHandler(Page_PreInit);
   45     }
   46 
   47 
   48     /// <summary>
   49     ///  Sets the MasterPageFile property for applicable pages at runtime.
   50     /// </summary>
   51     /// <param name="sender"></param>
   52     /// <param name="e"></param>
   53     void Page_PreInit(object sender, EventArgs e)
   54     {
   55         Page page = sender as Page;
   56         if (page == null) return;
   57 
   58         if (page is ISupportRuntimeMasterSwitching)
   59         {
   60             string runtimeMasterFile = ((ISupportRuntimeMasterSwitching)page).RuntimeMasterPageFile;
   61             if (!string.IsNullOrEmpty(runtimeMasterFile))
   62             {
   63                 page.MasterPageFile = runtimeMasterFile;
   64                 return;
   65             }
   66         }
   67 
   68         if (string.IsNullOrEmpty(page.MasterPageFile) && MasterCanBeApplied(page))
   69         {
   70             PagesSection pages = (PagesSection)ConfigurationManager.GetSection("system.web/pages");
   71             if (pages.MasterPageFile != string.Empty)
   72             {
   73                 page.MasterPageFile = pages.MasterPageFile;
   74             }
   75         }
   76     }
   77 
   78 
   79     /// <summary>
   80     ///  Attempts to determine whether the page is a content page (i.e. expecting a master page template)
   81     ///  or not.
   82     /// </summary>
   83     /// <param name="page"></param>
   84     /// <returns></returns>
   85     private bool MasterCanBeApplied(Page page)
   86     {
   87         if (page.HasControls())
   88         {
   89             if (page.Controls.IsReadOnly) return false;
   90 
   91             foreach (Control control in page.Controls)
   92             {
   93                 LiteralControl literal = control as LiteralControl;
   94                 if (literal == null || FirstNonWhiteSpaceIndex(literal.Text) >= 0)
   95                 {
   96                     return false;
   97                 }
   98             }
   99             return true;
  100         }
  101         return false;
  102     }
  103 
  104 
  105     private static int FirstNonWhiteSpaceIndex(string s)
  106     {
  107         for (int i = 0; i < s.Length; i++)
  108         {
  109             if (!char.IsWhiteSpace(s[i]))
  110             {
  111                 return i;
  112             }
  113         }
  114         return -1;
  115     }
  116 }
 

ISupportRuntimeMasterSwitching.cs
    1 using System;
    2 using System.Data;
    3 using System.Configuration;
    4 using System.Web;
    5 using System.Web.Security;
    6 using System.Web.UI;
    7 using System.Web.UI.WebControls;
    8 using System.Web.UI.WebControls.WebParts;
    9 using System.Web.UI.HtmlControls;
   10 
   11 /// <summary>
   12 /// Interface used by DefaultMasterPageModule to switch page MasterPageFile property
   13 /// if it implements this interface.
   14 /// </summary>
   15 public interface ISupportRuntimeMasterSwitching
   16 {
   17     string RuntimeMasterPageFile { get;}
   18 }
 

To use, add the module to your web.config:

    <httpModules>
      <add name="DefaultMasterPageModule" type="DefaultMasterPageModule, App_code"/>      
    </httpModules>  
Make sure your page declaration sets the MasterPageFile=””:

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

Then, you can either
a) Add an entry to your web.config system.web section like this:

    <pages
      masterPageFile="~/Nested.master"
      />

or
b) You can implement the ISupportRuntimeMasterSwitching interface on your page, and then set the RuntimeMasterFile property to whatever Master page you want to use. This also allows you to use a different master page on a single page than is configured in web.config.

Example Page that implements ISupportRuntimeMasterSwitching
    1 public partial class Default : System.Web.UI.Page, ISupportRuntimeMasterSwitching
    2 {
    3     protected void Page_Load(object sender, EventArgs e)
    4     {
    5 
    6     }
    7 
    8     #region ISupportRuntimeMasterSwitching Members
    9 
   10     public string RuntimeMasterPageFile
   11     {
   12         get
   13         {
   14             return "~/Test.master";
   15         }
   16     }
   17 
   18     #endregion
   19 }

Hope this helps someone else out there that likes to both have and eat cake.

Update 12/1/2005: Added a sample project for download. Also fixed a problem with code that prevented it from working properly with Server.Transfers.

Update 12/5/2005: No such luck with Server.Transfer / Server.Execute. I can’t seem to find any HttpApplication events that I can hook or a way to hook the page lifecycle events from the HttpModule in a way that will fire when a Server.Transfer or Server.Execute is called. If anybody has any suggestions let me know. Otherwise, understand that this is a limitation of this approach. If this is a gotcha for you, then you could consider applying the same concepts using a base page approach of course.

Leave a Comment