Click here to Skip to main content
Click here to Skip to main content

Straight way to create ASP.NET user controls library

By , 20 Mar 2009
Rate this:
Please Sign up or sign in to vote.

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?

  1. depending of hash of the Dll: backup unchanged Dll OR restore it before processing (in order to prevent double processing of the same file)

  2. call aspnet_compiler

  3. gather names of newly created Dlls (Asp_web_[control name].ascx.73dba69a.dll, etc )

  4. create "interface" Dll containing classes inherited from ASP.XX ones. These classes have names similiar to grandparents' ones.

  5. load special unmanaged resources from Dlls created by aspnet_compiler

  6. merge all these Dlls into one file (interface Dll + Dlls created by aspnet_compiler + initial Dll) using ILMerge tool

  7. write special unmanaged resource into resulting Dll

  8. overwrite Dll files within output and intermediate folders

  9. 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

    • You can see "Could not load type [strange type name] from assembly [assembly name]" error in runtime. It occures if you try to use members of an user control outside of the control. I.e. you can't create public properties/methods within your user controls directly. It is problem of the IlMerge tool. I can't fix it myself but i will try to notify ILMerge author.

      You can use following workaround: Create a base class for your user control and place all public members into the base class. See LibraryB.ControlC for the example.

    • It is impossible to create project references to signed libraries. DLL references are available only. It occures because the tool replaces output library and new library has different public key.

    • You can see "An assembly with the same identity 'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' has already been imported. Try removing one of the duplicate references.". This problem was solved in v0.6. Just download it.

    • Say me in case of any other problems. I will try to help you.

    Conclusion

    The tool fills a small gap in perfect ASP.NET platform.  I hope, this thing will be useful for your projects.

  • License

    This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

    About the Author

    Alexey Z1
    Software Developer (Senior) HRsoft
    Russian Federation Russian Federation
    No Biography provided

    Comments and Discussions

     
    BugVery Strange Problem PinmemberMember 1027744217-Sep-13 12:03 
    Questionpublic member/property thing Pinmemberray_hs7-Mar-13 4:20 
    Questionplease HELP Pinmembertalih19-Oct-12 13:15 
    AnswerRe: please HELP Pinmemberray_hs7-Mar-13 4:22 
    QuestionControls direct properties PinmemberMember 45926544-Sep-12 21:57 
    QuestionFramework 4.0 support Pinmemberunconnected5-Nov-11 3:19 
    AnswerRe: Framework 4.0 support Pinmemberunconnected28-Nov-11 20:16 
    Generalascx issues with aspnet_compile unmanaged resources Pinmemberholdingj7-May-09 14:18 
    GeneralRe: ascx issues with aspnet_compile unmanaged resources Pinmemberholdingj8-May-09 13:14 
    GeneralRe: ascx issues with aspnet_compile unmanaged resources PinmemberAlexey Zubritsky18-May-09 20:10 
    AnswerRe: ascx issues with aspnet_compile unmanaged resources Pinmemberholdingj27-May-09 12:13 
    Been working on this for awhile and this is the best I could come up with as of yet. Originally tried to do this using mono.cecil but they have some issues with generating a valid pdb in 3.5 right now. Basically I stored the offsets as we grab them from the original dlls and modified the offsets in the final dll then resaved. This might be the long term solution but like I said cecil has some issues right now. So here is my 2nd attempt.
     
    As we load resources calculate the offsets. Stored them off into a second byte array list. Pass that into the interfacebuilder and gen up a special class that looks like the code below marked #1 Example. Then have a base class that you inherit all your usercontrols from that call this method inside of AddParsedSubObject if it is either a ResourceBasedLiteralControl or a HtmlGenericControl. Using reflection we call into the method created in example #1 and viola the method updates the ResourceBasedLiteralControls private var _offset with the correct calculated value see #2 Example below. Again this would be simpler if we could modify the dll directly but no luck on that front yet. Example #3 contains the code in interfacebuilder to gen the method in #1. If u want I can give u the actual modified files. Just let me know where to put em. Also if you can think of a better way to do this let me know.
     
    #1 Example
     
    public class ResourceBasedLiteralUpdater
    {
    // Methods
    public static void UpdateResourceBasedLiteralControl_Offset(UserControl userControl, object obj)
    {
    int num = 0;
    FieldInfo field = obj.GetType().GetField("_offset", BindingFlags.NonPublic | BindingFlags.Instance);
    if (userControl is search_search_controller_ascx)
    {
    num = 0;
    num += (int) field.GetValue(obj);
    field.SetValue(obj, num);
    }
    if (userControl is details_details_controller_ascx)
    {
    num = 0x1e9;
    num += (int) field.GetValue(obj);
    field.SetValue(obj, num);
    }
    if (userControl is results_results_controller_ascx)
    {
    num = 0x385;
    num += (int) field.GetValue(obj);
    field.SetValue(obj, num);
    }
    }
    }
     
    #2 Example (inside a base class of all implemented usercontrols)
     
    protected override void AddParsedSubObject(object obj)
    {
    if ( (obj.ToString().Contains("ResourceBasedLiteralControl")) || (obj is System.Web.UI.HtmlControls.HtmlGenericControl) )
    {
    CallUpdate(obj);
    }
     
    base.AddParsedSubObject(obj);
    }
     

    private void CallUpdate(object resourceLiteral)
    {
    if (resourceLiteral is System.Web.UI.HtmlControls.HtmlGenericControl)
    {
    object newResourceControl = null;
     
    //need to find the real literal control inside of this guy.
    System.Web.UI.HtmlControls.HtmlGenericControl control = resourceLiteral as System.Web.UI.HtmlControls.HtmlGenericControl;
     
    if (control != null)
    {
    foreach (Control ctrl in control.Controls)
    {
    if (ctrl.ToString().Contains("ResourceBasedLiteralControl"))
    {
    newResourceControl = ctrl;
    break;
    }
    }
    }
     
    if (newResourceControl != null)
    resourceLiteral = newResourceControl;
    else
    return;
    }
     
    Assembly ass = this.GetType().Assembly;
    Type ctrlType = ass.GetType(ass.GetName().Name + ".ResourceBasedLiteralUpdater");
     
    if (ctrlType != null)
    {
    MethodInfo methodInfo = ctrlType.GetMethod("UpdateResourceBasedLiteralControl_Offset");
     
    if (methodInfo != null)
    {
    object[] param = new object[2];
    param[0] = this;
    param[1] = resourceLiteral;
     
    object instance = methodInfo.Invoke(this, param);
    }
    }
    }
     
    #3 Example
     
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Reflection;
    using Microsoft.CSharp;
    using System.CodeDom.Compiler;
    using System.IO;
     
    namespace WebLibraryMaker
    {
    class InterfaceBuilder
    {
    public static string Build(Pathes pathes, string[] assemblyPathes, List<byte[]> byteArray)
    {
    List<Assembly> assemblies = new List<Assembly>();
     
    foreach (string assemblyPath in assemblyPathes)
    {
    assemblies.Add(Assembly.LoadFrom(assemblyPath));
    }
     
    Assembly mainAssembly = assemblies[0];
    string mainName = mainAssembly.GetName().Name;
     
    CompilerParameters compilerParameters = new CompilerParameters();
     
    Dictionary<string, string> providerOptions = new Dictionary<string, string>();
    providerOptions.Add("CompilerVersion", "v3.5");
    CSharpCodeProvider CSharpProvider = new CSharpCodeProvider(providerOptions);
     
    //Create a new Compiler parameter object.
    compilerParameters.GenerateExecutable = false;
    compilerParameters.GenerateInMemory = false;
    compilerParameters.OutputAssembly = pathes.CompiledFileBillet + ".Interface.dll";
     
    //reference to System.dll is resuired in any case
    compilerParameters.ReferencedAssemblies.Add("System.dll");
    compilerParameters.ReferencedAssemblies.Add("System.Core.dll");
     

    //reference to each App_Web_*.dll assembly
    foreach (string assemblyPath in assemblyPathes)
    {
    compilerParameters.ReferencedAssemblies.Add(assemblyPath);
    }
     
    //reference to each file used by the initial library
    foreach (AssemblyName assemblyName in mainAssembly.GetReferencedAssemblies())
    {
    Assembly assembly;
     
    try
    {
    //standard assembly or GAC assembly
    assembly = Assembly.Load(assemblyName);
    }
    catch
    {
    //custom assembly
    assembly = Assembly.LoadFrom(assemblyName.Name + ".DLL");
    }
     
    if (String.Compare(Path.GetFileNameWithoutExtension(assembly.Location), "system", true) != 0)
    compilerParameters.ReferencedAssemblies.Add(assembly.Location);
    }
     
    StringBuilder code = new StringBuilder();
     
    foreach (Assembly assembly in assemblies)
    {
    foreach (Type type in assembly.GetTypes())
    {
    if (type.Namespace == "ASP" && !type.BaseType.Namespace.StartsWith("System."))
    {
    //create child class for each ASP.XX one
    code.AppendLine("namespace " + type.BaseType.Namespace + " {");
    code.AppendLine("public class " + type.BaseType.Name);
    code.AppendLine(": " + type.Name);
    code.AppendLine("{");
    code.AppendLine("}");
    code.AppendLine("}");
    }
    }
    }
     
    //Create static method that we can use to update the offsets.
    if (byteArray.Count > 0)
    {
    //create extension method
    //create child class for each ASP.XX one
    code.AppendLine("namespace " + pathes.ProjectName + " {");
    code.AppendLine("public class ResourceBasedLiteralUpdater");
    code.AppendLine("{");
     
    code.AppendLine("public static void UpdateResourceBasedLiteralControl_Offset( System.Web.UI.UserControl userControl, object obj)");
    code.AppendLine("{");
    code.AppendLine(@"
    int iOffSet = 0;
    System.Reflection.FieldInfo field = obj.GetType().GetField(""_offset"", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

    ");
     
    int iCnt = 0;
    int offSet=0;
     
    foreach (Assembly assembly in assemblies)
    {
    if (byteArray[iCnt] != null)
    {
    foreach (Type type in assembly.GetTypes())
    {
    if (type.Namespace == "ASP" && !type.BaseType.Namespace.StartsWith("System."))
    {
    code.AppendLine(@"
    if ( userControl is " + type.Name + @" )
    {
    iOffSet = " + offSet + @";
     
    iOffSet += (int)field.GetValue(obj);
    field.SetValue(obj, iOffSet);
    }");
     
    }
    }
    }
     
    if (byteArray[iCnt] != null)
    offSet += byteArray[iCnt].Length;
     
    iCnt++;
    }
    code.AppendLine("}");
    code.AppendLine("}");
    code.AppendLine("}");
    }
     
    if (code.Length != 0)
    {
    code.Insert(0, "using ASP;\r\n");
    }
     
    //compile the code
    CompilerResults compilerResult = CSharpProvider.CompileAssemblyFromSource(compilerParameters, code.ToString());
    if (compilerResult.Errors.Count > 0)
    {
    Console.WriteLine("Interface library creation error(s):");
    foreach (CompilerError error in compilerResult.Errors)
    {
    Console.WriteLine(error.ErrorText);
    }
     
    return null;
    }
     
    return compilerParameters.OutputAssembly;
    }
    }
    }
    GeneralRe: ascx issues with aspnet_compile unmanaged resources Pinmemberholdingj27-May-09 12:15 
    GeneralStrange error PinmemberBarbelith227-Mar-09 7:23 
    GeneralRe: Strange error PinmemberAlexey Zubritsky11-Mar-09 8:47 
    QuestionRe: Strange error PinmemberZauberer19-Mar-09 20:37 
    AnswerRe: Strange error PinmemberAlexey Zubritsky20-Mar-09 8:52 
    GeneralError when adding custom public properties in a library ascx Pinmemberholdingj8-Jan-09 11:59 
    GeneralRe: Error when adding custom public properties in a library ascx Pinmemberholdingj8-Jan-09 12:35 
    GeneralRe: Error when adding custom public properties in a library ascx PinmemberAlexey Zubritsky21-Jan-09 10:00 
    GeneralRe: Error when adding custom public properties in a library ascx PinmemberAlexey Zubritsky16-Feb-09 8:38 
    GeneralRe: Error when adding custom public properties in a library ascx Pinmemberarnoldo_aguilar4-Jun-09 12:44 
    GeneralStrong Name required Pinmemberredhotsoft26-Dec-08 14:48 
    GeneralRe: Strong Name required PinmemberAlexey Zubritsky21-Jan-09 9:11 
    GeneralRe: Strong Name required PinmemberAlexey Zubritsky16-Feb-09 8:41 
    QuestionRe: Strong Name required PinmemberZauberer20-Mar-09 14:15 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    | Advertise | Privacy | Mobile
    Web03 | 2.8.140421.2 | Last Updated 20 Mar 2009
    Article Copyright 2008 by Alexey Z1
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Use
    Layout: fixed | fluid