Using MSBuild and ILMerge to Package User Controls For Reuse

Thursday, October 6, 2005

One of the advantages to ASP.NET server controls is the ability to package them into an assembly and reference them from other web applications. Server controls are relatively difficult to write but easy to reuse. User controls (ascx files), on the other hand, are relatively easy to develop, but don't like to swing with other projects. A common solution in 1.x involves setting up virtual directories. Yuck.

The ASP.NET 2.0 environment is different. We have MSBuild. We have an ASP.NET compiler. Perhaps you've also noticed we have a tool by the name of ILMerge.

Here is a proof of concept.

Step 1: I created a new solution in Visual Studio 2005 and added a plain class library project. I then added two user controls to the project, which Visual Studio doesn’t like initially, but it does all work, even the intellisense. The first user control is all inline code:

<%@ Control Language="C#" ClassName="SayHello" >

<script runat="server">
  <protected void Page_Load(object sender, EventArgs e)
  {
    label.Text = "Hello at " DateTime.Now.ToShortTimeString();
  }       
</script>

<asp:Label runat="server" ID="label"/>

The second control, SayGoodbye.ascx, is the same, except it puts the Page_Load logic into a separate CodeFile by the name of SayGoodbye.cs.

Step 2: One of the cool features of Visual Studio 2005 is that the project files are MSBuild files. The default .csproj file for the class library I created does not know what to do with .ascx user control files, but I help it. I can right-click the project and select unload, then right-click the now disabled project and select Edit. Behold - the project is exposed naked before me. Pure angle-brackety goodness.

What I want to do at this point is modify the project to run the ASP.NET precompilation task – easy enough since there is an MSBuild task available by the name of AspNetCompiler. What is tricky is that AspNetCompiler will likely produce multiple assemblies. The compilation tool will batch compile by default, which means one assembly per directory of user control files. Of course we don’t want to keep all the ascx files in the root of the project, and we don't want to reference an entire directory of .dll files, so this is where ILMerge comes in.

ILMerge is a utility to merge multiple .NET assemblies into a single assembly. Combining AspNetCompiler and ILMerge together gives us something like the following:

<Project DefaultTargets="CompileUserControls" 
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
           ...
  <Target Name="CompileUserControls">
    <!-- TargetPath cannot be underneath PhysicalPath :( -->
    <AspNetCompiler 
      Debug="false"
      PhysicalPath="$(MSBuildProjectDirectory)" 
      TargetPath="$(TempDirectory)" 
      Updateable="false" 
      Force="true" 
      VirtualPath="$(MSBuildProjectName)" />

    <CreateItem Include="$(TempDirectory)\bin\*.dll">
      <Output ItemName="PrecompiledAssemblies" TaskParameter="Include" />
    </CreateItem>
    
    <Exec Command="$(ILMergeEXE) /out:$(MSBuildProjectName).dll 
                    /targetplatform:v2 @(PrecompiledAssemblies, ' ')" />
  </Target>
    ...

Inside the target I first run the AspNetCompiler and produce all the user control assemblies. Next, I need a list of all the assemblies the compiler just spit out. I do this with , which produces the Item PrecompiledAssemblies (thanks to Chris Tavares and the Chris Sells' Win-OT list for figuring this out). The last piece is to execute the ILMerge tool, passing all the assembly names as command line arguments.

Plop! Out comes ReusableControls.dll.

For the last step, I created a new web project and referenced ReusableControls.dll, then created a little test aspx web form.

<%@ Page Language="C#" %>
<%@ Register TagPrefix="rc" Namespace="ASP" Assembly="ReusableControls" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<form id="form1" runat="server">
   <rc:SayHello runat="server" ID="hello" />
  
   <br />
  
   <rc:saygoodbye_ascx runat="server" ID="goodbye" />
  <br />
</form>

Notice in the @ Register directive we need to specify a namespace of ASP, as this is the default given by the AspNetCompiler. The compiler also munges the user control name when a CodeFile is used – we are using the class that was code-generated from the SayGoodbye.ascx file and inherits from the SayGoodbye class in the CodeFile. With inline code there is no adjustment.

It might be possible to also re-use master pages and webforms with a little bit of VirtualPathProvider trickery, but I’m not going there as yet. I want to see how this will shake out at RTM time when combined with the Build Project that should arrive, as announced by others.


Comments
Stan Thursday, October 6, 2005
This rocks! but how did you get the MSBuild file into the solution explorer?
scott Thursday, October 6, 2005
The beauty of the system is MSBuild file == .csproj file, so VS2005 groks it. Once you edit the file by hand, just right-click and "reload project". VS2005 will run MSBuild against the project.
Raghu Friday, October 7, 2005
In 1.x, I had used virtual directories approach. This is really very cool way to reuse user control cross project.
Good digging scoot!!!
scott Saturday, October 8, 2005
The MSBuild team has the annoucement, too:

blogs.msdn.com/.../478425.aspx
scott Thursday, October 20, 2005
I'm having an interesting discussion with Guenter in the newsgroups, who is refining the proj file:

groups.google.com/.../c7b8dfc1c1adda17
Robert W. Anderson Thursday, October 20, 2005
Good article -- it gave me validation to try generally merging ASP.NET 2.0 assemblies for release. That is the article mentioned in the trackbacks.
Robert W. Anderson Thursday, October 20, 2005
P.S. Sorry about all of the trackbacks -- editing my blog caused this to happen automatically. Feel free to delete them, and this one for that matter. Cheers.
Scott Allen Friday, October 21, 2005
No problem, Robert! Glad you found it useful.
Keith J. Farmer Wednesday, October 26, 2005
Very cool.

Now we just need to work on that class naming problem...
Scott Monday, October 31, 2005
David Ebbo is also having a crack at this: blogs.msdn.com/davidebb/comments/487160.aspx

Paul V. Friday, November 11, 2005
Very interesting article. It would do just what I need. I just can't get it to work for user controls that rely on code in the same assembly or a remote one. What am i missing? :(
scott Friday, November 11, 2005
Paul: I haven't tried either of those scenarios yet. I'd think if the code the controls needed was in App_Code you'd be ok, as it would all get merged togeter.

I'm going to be taking a second look at this now that the web deployment project released.
Nicklas Johansson Monday, December 5, 2005
This is great, this helped me alot. The only problem I have right now is that the project is creating two .dll with the same classes in them. I'm kind of new to modding the Build-file, so I guess it's something very basic.

Any thoughts?
Michael Tuesday, December 13, 2005
Great article, it has set me on the right track! However I have a problem building custom controls with a codefile, as every server control that I use in the ascx file then generates a "does not exist in current context" error when I refer to it in the ascx.cs file.
So apparently the "link" between the .ascx file and the .ascx.cs file is lost. Any idea? Could you maybe post the code of your SayGoodbyeClass? Although I set the CodeFile, Inherits and ClassName attributes in the control definition, I probably forgot to set one or another attribute correclty... Thanks!
paul Thursday, May 18, 2006
Was the code for this ever submitted?
Graham Wednesday, July 26, 2006
I've just posted a new solution for .Net 2 that allows you to include UserControls, Pages and MasterPages in a single Web Application Project library that can be distrubed easily.

Its quick and easy to set up, there a description of how it works and a solution download that demonstrates everything working.

http://www.nearasmakesnomatter.co.uk/wapul.htm
gravatar whatispunk Monday, February 22, 2010
I'm missing something. Where do you define the $(ILMergeEXE) variable?
gravatar Aaron Prohaska Tuesday, February 23, 2010
This seems so simple yet I get a red squiggly under the class name where I have (from your example) <rc:SayHello. It acts like the compiler does not see SayHello. How can I make this work?
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!