Archive for November, 2008

November 3rd 2008

Creating Class Libraries of Asp.Net Web Components

Suppose you have multiple client projects and want to build a reusable framework for them. This framework could include components to build sites (server controls, data control fields, custom datagrid, custom pager etc.). It could also include components such as pages, user controls, and web services.

Scott Gu espouses a strategy for creating user control libraries here, here, and here. The technique involves copying the makrup files from the library “project” to the client “project” using xcopy (robocopy on vista). There are additional articles like this one describing even more ways to achieve partial solutions to this problem.

Depending on your development and deployment needs the solution to this problem will vary. In this article I’ll go through some of the choices available their related trade-offs, give sample applications demonstrating various approaches, and show another method to do this without copying markup files by referencing only the fully compiled library site.

If your library is developed as a web site you can leverage the edit-and-continue feature to create and debug components more quickly. Conversely, since there is no assembly associated with a web site, a web deployment project is needed before components can be referenced in a client application.

Note: In order for components in the app_code directory of the library assembly to be accessible to the client application, library assembly must be referenced by the client application and the setting “Treat As Library Component (remove the App_Code.compiled file)” must be selected in the web deployment project.

By copying the component files to the client application, the library components containing markup are in effect being compiled as part of the client application. This can, however, lead to some issues.

If the web deployment project has “Allow this precompiled site to be updateable” checked, the build will remove the markup from aspx and ascx files and replace them with the text “This is a marker file generated by the precompilation tool, and should not be deleted!”. When the client application builds, Visual Studio tries to build these marker files and  fails on build with an error similar to “The page must have a <%@ webservice class=”MyNamespace.MyClass” … %> directive.”

When copying the markup files directly from the library “project” the files may contain a “codeFile” attribute which will not exist in the client application, causing a compiler warning. Alternatively, when the markup files are copied from the web deployment project’s output, the codeFile attribute has been removed but the compiler will now warn “c:\…\v2.0.50727\Temporary ASP.NET Files\…\App_Web_x4mc84hf.0.cs(134,53): warning CS0108: ‘ASP.library_usercontrols_libraryusercontrol_ascx.Profile’ hides inherited member ‘LibrarySite.Library.UserControls.LibraryUserControl.Profile’. Use the new keyword if hiding was intended.” This is because the web application project already compiled part of the markup into the library assembly, namely the “Profile” and “ApplicationInstance” properties.

To enable web deployment compilation of the client application, the markup files must be copied from the library project (with the codeFile attribute) and not from the web deployment project output, otherwise Aspnet_compile will error.

Another common error is “Compiler Error Message: CS0433: The type ‘ASP.library_usercontrols_libraryusercontrol_ascx’ exists in both ‘c:\…\v2.0.50727\Temporary ASP.NET Files\…\App_Web_empkj51o.dll’ and ‘c:\…\v2.0.50727\Temporary ASP.NET Files\…\assembly\…\LibrarySite.DLL’” This again stems from the re-compilation of library controls in the client application when the client application already references the library assembly. It stems from a situation where the user control/page/webservice has been compiled in the library assembly and is compiled into the client application with the same type name.

A sample of this setup where the library is a web site can be found here (Client application is a vs2k8 web site) and here (Client application is a vs2k8 web application project).

A sample of this setup where the library is a web application project can be found here (Client application is vs2k8 web site) and here (Client application is a vs2k8 web application project).

If you’re using subversion, one final approach to this method involves using svn externs.  An extern link to a child directory of the library is created from a child directory of the client application. When the Client application was compiled, it would also compile these files. Since the client application does not reference the library assembly, there is no type conflict and the controls are only compiled once.

This solution does not work for web services and is quite inflexible.

The basic problem with these approaches is the recompilation of the library controls and the tedious copying of the markup files.

A more flexible approach is to fully compile the library into a single assembly and reference it from the client application as a standard assembly. The problem now is to make the pages, user controls, and web services available to the client application.

In .net 2.0, user controls can be referenced by assembly and namespace in addition to url via the LoadControl method. When referencing precompiled user controls, the key is to reference the compiled markup class, not the codebehind (ASP.library_usercontrols_libraryusercontrol_ascx instead of LibrarySite.Library.UserControls.LibraryUserControl).

To reference pages and web services in the library assembly we need to update our configuration to map requests to these resources to the correct handlers.

In HttpHandlers section of the client application web.config we replace the existing handlers for aspx and asmx as follows:

<remove verb="*" path="*.asmx"/>
<remove verb="*" path="*.aspx"/>

<add verb="*" path="*.aspx" type="LibrarySite.PageHandlerFactory, LibrarySite" validate="true" />
<add verb="*" path="*.asmx" validate="false" type="LibrarySite.AutoDiscoveryWebServiceHandlerFactory, LibrarySite"/>

Now all http requests for .aspx and .asmx files will go through custom IHTTPHandlerFactories to dynamically load pages and web services from the library assembly as necessary. The IHTTPHandlerFactories receives requests and tries to match them to the resource against the full type names in loaded assemblies (e.g. http://localhost/ClientSite/LibrarySite.Library.Pages.LibraryPage.aspx will cause the IHTTPHandlerFactory to load and execute the page LibrarySite.Library.Pages.LibraryPage). If it is unable to find a match, the IHTTPHandlerFactory returns the result of the base (standard) IHTTPHandlerFactory.

The web service implementation for this came from this fantastic posting on codeproject: http://secure.codeproject.com/KB/cpp/WebserviceAndJavaProxy.aspx?display=Print. The author took great care to ensure his solution worked with JSON enabled web services which is very helpful.

In contrast to the web services code, the IHTTPHandlerFactory is much simpler:
Note: the framework caches the mappings between virtualPath and the IHTTPHandler returned so the probe for matching page handlers is only done once per virtualPath.

public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
{
 // Try to get the type associated with the request (On a name to type basis)
 Type pageType = this.GetPageServiceType(Path.GetFileNameWithoutExtension(path));
 // if we did not find any send it on to the original ajax script service handler.
 if (pageType == null)
 {
  return base.GetHandler(context, requestType, virtualPath, path);
 }
 else
 {

  return (Page)Activator.CreateInstance(pageType);
 }
}

  /// <summary>
  /// Searches all Services and tries to find a class with the specified name
  /// </summary>
  private Type GetPageServiceType(string pageTypeName)
  {
   // Todo: Caching mechanism for assembly checks
   foreach (Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
   {
    Type basePageType = loadedAssembly.GetType(pageTypeName);
    if (basePageType != null)
    {
     var services = from t in loadedAssembly.GetTypes()
           where t.IsSubclassOf(basePageType)
            select t;
     var pageType = services.FirstOrDefault();
     if (pageType != null)
     {
      return pageType;
     }
    }
   }
   return null;
  }

A working sample of all of this is here.

 

 

No Comments yet »