Wednesday, December 5, 2007

Creating an ASP.NET page from code

As a control developer I sometimes need to create a page purely from code, without any .aspx page and behind-the-scenes compilation by ASP.NET. This introduces some issues because the compilation of an .aspx file introduces some code that is not immediately obvious. In this article I've tried to compile a list of things needed to make pages work from code.

The basics
A page is an IHttpHandler so it can handle a request through it's ProcessRequest method. This is where the ASP.NET Page lifecycle starts. One of the first things a 'normal' page does is construct it's control tree. It does this through the FrameworkInitialize method. Normally the ASP.NET compiler will generate code to construct the control tree for the page. Since we're building the page from scratch, we need to do this ourselves.

Building a control tree
There are a couple of basic things every ASP.NET page has:

  • Document type:<!DOCTYPE html .... >
  • Basic HTML structure: <html> etc.
  • A header control ( HtmlHead )
  • A form control ( HtmlForm )
For an .aspx page, the compiler will take care of creating these. For a programmatic page, this has to be hand coded:
protected override void FrameworkInitialize()
{
  base.FrameworkInitialize();
  AddParsedSubObject( new LiteralControl( 
      "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 
       Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/ 
       xhtml1-transitional.dtd\">\r\n" ) );
  AddParsedSubObject( new LiteralControl( 
      "<html xmlns=\"http://www.w3.org/1999/xhtml\" >\r\n" ) );  
  AddParsedSubObject( new HtmlHead() );   AddParsedSubObject( new LiteralControl( "\r\n<body>\r\n" ) );   HtmlForm form = new HtmlForm();   form.ID = "form1";   AddParsedSubObject( form );   // Add any other child controls to form.Controls here   AddParsedSubObject( new LiteralControl( 
      "\r\n</body>\r\n</html>" ) );
}

Note that controls on the page must be added as child controls of the Form control, just like controls in an .aspx file are declared within the form.

Page directives
Most of the @Page directives translate directly into code in one way or another. Below a list of the most commonly used directives. ResponseEncoding FrameworkInitialize is a good place to set the response encoding. If you set it here, you can be sure that nothing has been written to the response stream yet.

Response.ContentEncoding = Encoding.UTF8;
Session state To enable session state, make sure your page class implements one of the following interfaces:
Full access IRequiresSessionState
ReadOnly IReadOnlySessionState
If the Page class does not implement one of these marker interfaces no session state will be available in the page. Note that if you're not modifying session state from the page, read only access may give a performance benefit.
Theme Assign the theme name:
Theme = "myTheme";
ValidateRequest To validate the request, invoke
Request.ValidateInput();

Note that the default value of ValidateRequest is true, so unless you explicitly do not want your requests validated, invoke ValidateInput to be on the safe side.

Adding support for Ajax
If you need Ajax support on the page, insert an Ajax ScriptManager control as the first control in the form.

// Add Ajax Script Manager
form.Controls.Add( new ScriptManager() );

ResolveClientUrl
Some of the internal methods of the page rely on information obtained from the compiler. Most notably functions like ResolveClientUrl. In order to make these work, set the AppRelativeTemplateSourceDirectory property.

protected override void OnInit( EventArgs e )
{
  if ( string.IsNullOrEmpty( AppRelativeTemplateSourceDirectory ) )
  {
// This is required to make ResolveClientUrl function correctly
// when the page is invoked as an HttpHandler.
// Note that this assumes the url for the http handler // definition is in the application root AppRelativeTemplateSourceDirectory = "~/"; } base.OnInit( e ); }

DataBinding
Databinding is a process mostly facilitated by the ASP.NET compiler. So, for pages like this, you pretty much have to hand code all databinding behavior. Luckily databinding is no rocket sience; the basic way of working relies on the DataBinding event. When you create a control that needs databinding, add a handler to it's DataBinding event. Then, in the handler, do what ever you need to set the databound properties. The control itself is passed in as the sender.

Serving up a virtual URL
Finally the page needs to be hooked up to a URL. This is possible through web.config. The easiest way is to add an entry to the handlers section:

<configuration>   <system.web>
    <httpHandlers>     <add verb="*" path="mypage.ashx" type="Alanta.Example.CustomPage"/>
    </httpHandlers>   </system.web>
</configuration>

You can handle any URL that is mapped to ASP.NET in this way (.aspx, .ashx etc.).

What's next ?
After constructing the page and adding your controls everything works as usual. The page will go through the normal Page lifecycle.

Wednesday, October 24, 2007

Debugging VS.NET Design time

Just came across an excellent article on Brennan's Blog explaining in detail how to setup Visual Studio to enable debugging of design time code. In short here's what it boils down to:
  1. In Visual Studio set the debug properties on the (Class Library) project you want to debug to 'Launch an external program' and make that program Visual Studio. In most cases that would be : C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe
  2. Save the properties and setup your breakpoints. Now right click the project and from the popup menu choose Debug > Start new instance.
  3. A new instance of Visual Studio fires op. Now, in the new instance, load a simple project that uses the project you want to debug and you're good to go.
This even helps debug build issues in things like VirtualPathProviders and ResourceProviders. Read the full article

Monday, October 1, 2007

The mysterious problem with WebResource.axd

Ever since I've started using embedded resources I've been running into this problem:

The WebResource.axd handler must be registered in the
configuration to process this request.
It occurs when an application is updated with an assembly that uses embedded resources.

It's a rather nasty problem because it will persist until the worker process is fully restarted. This is because the exception is triggered by a static field that is evaluated only once (!). Fredrik Haglund describes the mechanism here. Fredrik also offers a workaround, but it's a dirty one using reflection to tweak private variables. This work around will probably only work in full trust sites and may break with future updates of .NET. It does however get the job done for now.

Here's the code (slightly modified from Fredrik's version):
public static void WebResourceHandlerFix()
{
Type type = typeof( System.Web.Handlers.AssemblyResourceLoader );

System.Reflection.FieldInfo handlerExistsField =
type.GetField( "_handlerExists",
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static );

System.Reflection.FieldInfo handlerExistsCheckedField =
type.GetField( "_handlerExistenceChecked",
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static );

if ( null != handlerExistsCheckedField &&
null != handlerExistsField )
{
if ( ( bool )handlerExistsCheckedField.GetValue( null ) &&
    !( bool )handlerExistsField.GetValue( null ) )
   handlerExistsCheckedField.SetValue( null, false );
}
}
}

Note: This problem can also occur when trying to map URL's to a handler through a section in web.config. For example, this may cause problems:

<location path="virtualpage.aspx">
<system.web>
 <httphandlers>
    <add path="*" verb="*" type="Example.Web.Customhandler" />
 </httphandlers>
</system.web>
</location>

Because it will override the handler for WebResource.axd. Instead, try to add the mapping for the virtual page to the main <httphandlers> section in web.config.

Friday, September 28, 2007

Extending BoundField

After working with ASP.NET 2.0 on a couple of projects, one thing keeps biting... Databinding is very powerful but it's also very cumbersome. Even simple things like adding validation to a templated databound control (DetailsView, GridView) is horrible because it requires turning BoundFields into TemplateFields. Now, the BoundField I like; it's clean, simple and helps keep the UI code to a minimum. The TemplateField is an end to a means but it's not the way to go for complex controls. Don't even get me started on maintaining code that uses the TemplateField and all sorts of inline trickery. Believe me; been there done that and did NOT like it. So, what is it that's lacking from the basic fields for databinding? Well, for starters, there's no support for validation. There is no way to control the way a field is rendered; what if I wanted multi line text? or tune the width of the field? There is no way to drop in one of those nifty Ajax control extenders. You may also have noticed that data formatting by the BoundField is quirky to say the least, to say it's broken would be more accurate. Finally, to top it off, there is no such thing as a DropDownField. I'll save that for another post.

Pimp my DataBoundField
Now... what can we do to relieve the pain? Preferably without writing a new BoundField from scratch; we just want to expand it's capabilities. As usual, Lutz Roeders .Net Reflector helps us get a good insight into how BoundField and it's derivatives actually work. A quick peek under the hood reveals a good starting point to hook up some nice customizations:

protected virtual void InitializeDataCell(DataControlFieldCell cell, DataControlRowState rowState) I won't bore you with the details but this method is invoked after the field is created (through InitializeCell) and needs to have the DataCell (the container that will hold the controls that render the field) configured. The default implementation will look for a text box control and configure it for databinding. The text box is, not surprisingly, the control used to render the BoundField in edit and insert mode. With a handle to the TextBox and a way to get to it's container it looks like we have all the requirements for a quick episode of Pimp My DataBoundField.
Note: You could instead override InitializeCell. However, that method is invoked for all types of cells including footers, headers etc. InitializeDataCell is called only on cells that will render actual data so no extra code is needed to detect what type of cell we're working on.

Overriding BoundField
Overriding the BoundField is nothing special, just make sure you override the CreateField method as well to return your special flavour of BoundField.

protected override DataControlField CreateField()
{
return new MyBoundField();
}
Adding validation
With the insight gained above it's actually quite simple to add validation support to BoundField.
protected override void InitializeDataCell(
DataControlFieldCell cell,
DataControlRowState rowState )
{
base.InitializeDataCell( cell, rowState );

// Find the textbox
TextBox text = FindTextBox( cell );

// Could be null if field is readonly or not in edit or insert mode
if ( null != text )
{

// setting the id to help the validator find the field
text.ID = DataField;

// Add a RequiredFieldValidator
RequiredFieldValidator required =
  new RequiredFieldValidator();
required.ErrorMessage = "This is a required field";
required.Display = ValidatorDisplay.Dynamic;
required.ControlToValidate = text.ID;
required.Text = "";
cell.Controls.Add( required );
}
}

You can of course add all kinds of properties to configure the control (Required, ErrorMessage etc.).

Custom validation
In addition to the RequiredFieldValidator you could add support for custom validation using a CustomValidator. By exporting the ServerValidate event on the field you can declaratively bind a validation handler in the page to the field.

<alanta:customvalidatingfield datafield="name" headertext="Name"

onvalidatefield="Validate"/>

Check the sample code for more details.

More cool features
As you can see, it's quite easy to customize the BoundField with some useful features. Adding support for multi line text and specifying the width and height of the input field is straight forward since we have direct access to the TextBox. Add the appropriate properties and tweak the TextBox control in the InitializeDataCell method. Same goes for the Ajax Control Extenders from the Ajax Control Toolkit. Add the extender you need in the InitialzeDataCell and presto.

Date field
A more interesting data field control can be created by combining these techniques. A date field could benefit from a CalendarExtender so users can easily pick a date. Validation can help enforce the entered date is valid and within an allowed range. Please check the sample code for details.

Thursday, August 30, 2007

Output paths in Web Application Projects (VS.NET 2005)

I've recently started using the Web Application Project (WAP) in Visual Studio 2005 to allow me to create assemblies containing controls, including User Controls.

Works like a charm. I'm embedding the layout code (.ascx) as a resource and using a VirtualPathProvider to extract the resource for compilation in the site.

All nice and good. Until I started converting my older projects to WebApplication projects. For some reason the designer was refusing to render controls from external assemblies (like ASP.NET Ajax controls).

It was giving me nothing but gray boxes saying "Error Creating Control". No indication as to what was wrong... Turns out the designer expects all referenced assemblies to be in the /bin folder. Makes sense for a web project, but I'd expect VS to use the output folder for the current target (Release, Debug etc.) in a WAP.

Anyway, I've updated the output paths for all targets and all works fine now.

Tuesday, April 3, 2007

Finally... no more NDoc troubles

After spending way too much time being frustrated with NDoc and some of it's 'upgraded' versions crashing on my brand new generic classes, I've finally found what I need to get rid of NDoc all together.

Apparently it was right under my nose all this time! Microsoft is working on a tool called Sandcastle. It's a bunch of tools and XSL's used to transform XML code comments into an HtmlHelp project.

Since there's no official UI yet I've tried to build a NAnt script that runs all the tools and transforms in the right order but that did not yield a quick result. Fortunately a good Sandcastle UI is already available from the community. It looks just like NDoc so it's pretty much a click and go exercise. Just what I needed!

The final recipe (so far):

I hope this saves at least one other developer from going through the same problems I have!

Update - Managed to get the NAnt build working after all. If anyone wants the script I'll post it here. - It appears NDoc is officially dead. There will be no 2.0 release unless someone picks up where Kevin Downs stopped...

Friday, March 16, 2007

Stream to stream copy

John Skeet wrote an excellent article about reading binary data a while back. Thanks to John I now know that the Read method doesn't necessarily return the entire stream contents. This has saved me a bunch of trouble so far. However I run into a situation from time to time where I need to copy one stream into another. The input stream is potentially very large, so simply copying the whole thing into a byte array and then writing it to the output stream doesn't seem like a good idea. I've adopted some of John's sample code into this:
public static void StreamCopy( Stream input, Stream output )
{
 byte[] buffer = new byte[ 2048 ];
 int remaining = (int)input.Length;
 BinaryWriter writer = new BinaryWriter( output );
 while ( remaining > 0 )
 {
   int read = input.Read( buffer, 0,
     ( remaining < buffer.Length ? remaining : buffer.Length ) );

   if ( read <= 0 )
     throw new EndOfStreamException( "Unexpected end of stream" );

   writer.Write( buffer );
   remaining -= read;
 }
}
I can't help wondering if there is a more efficient way to do this...

Wednesday, February 21, 2007

Doooh... : Switching browsers for ASP.NET development

Just a quick post on swiching browsers for debugging in VS.NET 2005:
What you do to fix the issue is right click on the web project and from the pop up menu that appears choose "Browse with...". This displays a handy dialog that shows you all the web browsers registered on your machine. From there, just click on [your browser of choice], and then click on the "Set as default" button. From that point forward VS2005 will run [that] as the browser of choice whenever you hit F5 to run and debug your web application
Note that you may need to add your browser manually if it's not listed yet. Original post on Pete Wright's blog

Wednesday, January 24, 2007

ASP.NET 2.0: Using .refresh files from NAnt

I'm moving more and more towords building with NAnt, even for web applications. Based on some work done by Ewout from TallApplications I'm trying to develop a customizable NAnt build script for .NET 2.0 web applications. One of the things that was bothering me is that the aspnet_compiler tool (used to precompile sites) doesn't use the .refresh files used by Visual Studio 2005 to keep referenced assemblies up to date. It should be no problem to process these files with NAnt though; all they contain is the relative path to the referenced assembly in plain text. Google doesn't give quick results on this, so I went to work. Being a bit of newbie to NAnt build scripts it didn't take long to come up with this:
<!-- Refresh referenced assemblies -->
<foreach item="File" property="filename">
 <in>
    <items>
       <include name="${proddir}\bin\*.refresh" />
    </items>
 </in>
 <do>
    <loadfile file="${filename}" property="RefreshFile" />
    <echo message="Refreshing ${RefreshFile}"/>
    <regex input="${RefreshFile}" pattern="^(?<BaseFile>.*)\.dll"/>
    <copy todir="${proddir}\bin" verbose="true" flatten="true">
       <fileset>
          <include name="${proddir}\${BaseFile}.dll"/>
          <include name="${proddir}\${BaseFile}.xml"/>
          <include name="${proddir}\${BaseFile}.pdb"/>
       </fileset>
    </copy>
 </do>
</foreach>
Only requirement is that you set the proddir variable to the root folder of the site.