Straight way to create ASP.NET user controls library
My post-build tool transforming any
WebApplication into a library containing .ascx controls.
Home page of the tool
Preamble
I feel necessity to compile my ASP.NET user
controls into a library. It is good for modularity and reusing. However, there
is no official way to do it. We can create custom controls library only.
Note the difference between custom
controls and user controls. Custom control is ordinary .NET class inherited
from System.Web.WebControls.WebControl
. It overrides method RenderContents
and
renders itself with output.Write(...)
.
User control consists of *.ascx file,
codebehind * .vb file and *.designer.cs file. It allows to simple write HTML
code and uses all benefits of codebehind model. ASP.NET compiles it at runtime.
I researched many articles offering several
workarounds like
All these ways have seriously weakness. I
have several requirements for straight way:
- Preserve designers, code generators, etc working
within the library project
- It should be easy to use controls of the library
in any other web application
- It should be easy to use controls of the library
in other such libraries
- It should be easy to reuse controls of the
library within itself
- It should be possible to reference the library
as project
- It should be possible to reference the library
as DLL
- The library should consist of one file.
- It should be possible to create new library in
30 seconds
- The solution should work for .NET v2.0, v3.0,
v3.5
Solution
I base my solution on the idea of K. Scott
Allen: compile the project with AspNetCompiler and then use ILMerge. In
addition, my tool creates additional DLL containing classes inherited from
mycontrols_goodcontrol_ascx
, coolcontrols_thebestcontrol_ascx
, etc. Moreover,
these inherited controls have the SAME names as codebehind classes!
In action
How to create new library
- Extract WebLibraryMaker to somewhere on your PC
- Create new WebApplication project
- Select project root, press F4 and select Always
Start When Debugging = false
- Edit post-build step on property pages: Input
something like
"$(MSBuildProjectDirectory)\..\WebLibraryMaker\WebLibraryMaker.exe"
/net " $(Framework20Dir) " /name " $(MSBuildProjectName) "
/prj " $(MSBuildProjectDirectory) " /obj " $(IntermediateOutputPath)
" /out " $(OutDir) " /debug $(DebugSymbols) /key " $(AssemblyOriginatorKeyFile) " where
$(MSBuildProjectDirectory)\..\WebLibraryMaker\WebLibraryMaker.exe is path to
your WebLibraryMaker directory
- Make sure you have directory mentioned in
WebLibraryMaker.exe.config -> TemporaryPath setting (C:\temp\ASP.NET.Tmp by
default)
- Build the project
I’ve created two libraries in my example:
LibraryA
and LibraryB
:
LibraryA
and LibraryB
have 'Always Start When Debugging' = false:
ControlA
and ControlB
are very similar and simple. Each of them contains one textbox:
<%@
Control Language="C#" AutoEventWireup="true" CodeBehind="ControlA.ascx.cs" Inherits="LibraryA.ControlA"
%>
<asp:TextBox ID="TextBox1"
runat="server">ControlA</asp:TextBox>
ControlC
is more complex. I'll describe it
later.
I also added post-build step for LibraryA
and LibraryB
:
"$(MSBuildProjectDirectory)\..\WebLibraryMaker\WebLibraryMaker.exe"
/net " $(Framework20Dir) " /name " $(MSBuildProjectName) "
/prj " $(MSBuildProjectDirectory) " /obj "
$(IntermediateOutputPath) " /out " $(OutDir) " /debug
$(DebugSymbols) /key " $(AssemblyOriginatorKeyFile) "
It is content of solution directory (note,
there is WebLibraryMaker directory):
I also have 'C:\temp\ASP.NET.Tmp' directory
for temporary files.
Now you can press Build and viola! Two
libraries are ready.
How to use the library
- Link the library to your target project
- Add reference to the library project
- OR add reference to the library dll
- Use library controls like Custom Controls (control
type names are equal to codebehind type names):
- Use <% register assembly="yourassembly"
namespace="yournamespace" tagPrefix="your_prefix" %>
within .ascx/.aspx files
- Use
new yourassembly.yourcontrol()
within *.vb
files.
In my example LibraryB
uses LibraryA
.
MainApplication
uses LibraryB
. All references are created as project links.
LibraryB.ControlC
uses LibraryA.ControlA
.
Both static and dynamic control creation methods are used.
Static control creation (ControlC.ascx):
<%@
Register Assembly="LibraryA" Namespace="LibraryA" TagPrefix="LibraryA" %>
...
<LibraryA:ControlA runat="server"
ID="ControlA"/>
Dynamic control creation
(ControlC.ascx.vb):
new LibraryA.ControlA();
Default.aspx page of MainApplication
uses
ControlC
:
<%@
Register Assembly="LibraryB" Namespace="LibraryB" TagPrefix="LibraryB" %>
...
<LibraryB:ControlC runat="server"
ID="ControlC"
/>
How to reuse controls within the same
library
- Use library controls like User Controls within
.ascx/.aspx files
- Use <% register
src="~/yourpath/yourcontrol.ascx" tagPrefix="yourprefix"
tagName="yourcontrol"%>
- Use
Activator.CreateInstance()
within *.vb files. It is not possible to use Control.LoadControl
function. You can also use something like ControlLoader
class from the source example to speed-up this operation.
- Use
(Control)Activator.CreateInstance(Type.GetType("your_control"))
- OR Use
ControlLoader.LoadControl<your_control>
LibraryB.ControlC
uses LibraryB.ControlB
also. Both static and dynamic control creation methods are used again.
Static control creation (ControlC.ascx):
<%@
Register Src="~/ControlB.ascx" TagName="ControlB" TagPrefix="LibraryB" %>
...
<LibraryB:ControlB ID="ControlB"
runat="server"
/>
Dynamic control creation
(ControlC.ascx.vb):
(ControlB)Activator.CreateInstance(Type.GetType("LibraryB.ControlB"))
Signing (v0.6 only)
New version of the tool allowes to create signing assemblies. It supports two additional parameters: /key and /ds. There are two signing modes: regular and delayed.
- Regular mode. Just create signed assembly with standard VS functionality. Make sure that /key " $(AssemblyOriginatorKeyFile) " agrument is used
- Delayed signing mode. Don't create signed assembly with standard VS functionality. Only create public key.
Change /key argument to use real key filename like /key " publickey.snk ". Also specify /ds true. Resulting command example: "$(MSBuildProjectDirectory)\..\WebLibraryMaker\WebLibraryMaker.exe"
/net " $(Framework20Dir) " /name " $(MSBuildProjectName) "
/prj " $(MSBuildProjectDirectory) " /obj "
$(IntermediateOutputPath) " /out " $(OutDir) " /debug
$(DebugSymbols) /key " publickey.snk " /ds true
How does it work?
- depending of hash of the Dll: backup unchanged
Dll OR restore it before processing (in order to prevent double processing of
the same file)
- call aspnet_compiler
- gather names of newly created Dlls (Asp_web_[control
name].ascx.73dba69a.dll, etc )
- create "interface" Dll containing
classes inherited from ASP.XX ones. These classes have names similiar to
grandparents' ones.
- load special unmanaged resources from Dlls
created by aspnet_compiler
- merge all these Dlls into one file (interface
Dll + Dlls created by aspnet_compiler + initial Dll) using ILMerge tool
- write special unmanaged resource into resulting
Dll
- overwrite Dll files within output and
intermediate folders
- calculate hash code of the Dll
I.e. the tool merges following DLLs:
- initial Dll containing codebehind classes
- output of aspnet_compiler
- "interface" Dll containing classes
inherited from ASP.XX ones
ILMerge renames codebehind classes with
random names and referencing assemblies start use 'interface' classes instead
of codebehind ones.
ILMerge also copies managed resources from initial
Dll into output Dll
Additional problem occurs because VS copies
compiled Dll into intermediate folder (obj/Debug or something like it). Then
“smart” compiler uses this copy if no changes were done in *.vb files. The tool
calculates hash code to prevent double processing of the same file. Also the
tool makes its own backup copy of the initial Dll. This backup is used instead
of copy from obj/Debug to rebuild. (The tool rebuilds library each time, even
there was not changes in .vb files. It is necessary in cases when *.ascx files
were changed only.)
Unmanaged resources processing is necessary
because aspnet_compiler puts large texts from *.ascx files into special
unmanaged resources.
output of aspnet_compiler
"interface" Dll containing classes
inherited from ASP.XX ones
Known problems
Conclusion
The tool fills a small gap in perfect
ASP.NET platform. I hope, this thing will be useful for your projects.