Creating Reusable Content in ASP.NET
In This Chapter
Techniques for Creating Reusable Content
Building a ComboBox User Control
Using the ComboBox Control
Populating the ComboBox Control
Editing the Connection String
Although the general public's view of computer programmers as a breed apart might be less than complimentary, we are really no different from any other people when it comes to having a hatred of dull, repetitive work. When writing code, experienced programmers are constantly on the lookout for ways to encapsulate chunks that are reusable and save the effort of having to write the same code repeatedly. Subroutines and functions are obvious examples of ways to do this within a single application; components, DLLs, and .NET assemblies provide the same kind of opportunities across different applications.
However, when building Web pages and Web-based interfaces for your applications, it can be difficult to choose the obvious or the most efficient approach for creating reusable content. Traditional techniques have been to read from disk-based template files and to use disk-based include files that rely on the server-side include feature of most Web server systems.
Of course, the use of external code in the form of COM or COM+ components, and in ASP.NET, the use of .NET assemblies, is also prevalent in Web pages. However, the complexity of the plumbing between COM/COM+ components and the host application has never really been an ideal approach when working with Web pages that have extremely short transitory lifetimes on the server. These components work much better when instantiated within an executable application where they have a longer lifetime.
In ASP.NET, the ideal solution from a component point of view is to use native .NET managed code assemblies. These are, of course, the building blocks of ASP.NET itself, and they provide the classes that implement all the ASP.NET controls we use in our pages. However, the .NET Framework provides several techniques that are extremely useful and efficient and that can provide high levels of reuse for interface declarations and runtime code.
Techniques for Creating Reusable Content
Before delving too deeply into any of the specific techniques for creating reusable content, we'll briefly summarize those that are commonly used within ASP.NET Web applications:
Server-side include files
ASP.NET user controls
Custom master page and templating techniques
ASP.NET server controls built as .NET assemblies
Using COM or COM+ components via COM Interop
Server-Side Include Files
Many people shun the use of server-side includes in ASP.NET, preferring to take advantage of one of the newer and flashier techniques that are now available (such as user controls, server controls, and custom templating methods). However, server-side includes are just as useful in ASP.NET as they are in "classic" ASP. They are also more efficient than in ASP because ASP.NET pages are compiled into an assembly the first time they are referenced, and this assembly is then cached and reused automatically until the source changes.
As long as none of the files on which an assembly is dependent change (this applies to things like other assemblies and user controls as well as to server-side include files), the page will not be recompiled. This means that the include process will be required only the first time the ASP.NET page is referenced, and it will not run again until recompilation is required. The content of the include file becomes just a part of the assembly.
Of course, the same include file is likely to be used in more than one page. Any change to that file will therefore cause all the assemblies that depend on it to be recompiled the next time they are referenced. This makes include files extremely useful for items of text or declarative HTML that are reused on many pages but that change rarely. An example is a page footer containing the Webmaster's contact details and your copyright statement.
Using Server-Side Include Files to Insert Code Functions
Remember that you aren't limited to just using text and HTML in a server-side include file. You can place client-side and server-side code into it and, in fact, you can put in it any content that you can use in an ASP.NET page. This means you can, for example, place just code routines into a server-side include file and then call those functions and subroutines from other code in the main hosting page, or you can even call them directly from control events. However, you can only include files that are located within the same virtual application as the hosting page.
Including Dynamic Text Files in an ASP.NET Page
Another area where server-side include files are useful is where you have some dynamically generated text or HTML content that you want to include in a Web page.
One particular example we use ourselves is to remotely monitor the output generated by a custom application that executes on the Web server. It generates a disk-based log file as it runs and allows the name and location of the log file to be specified. We place the log file in a folder that is configured as a virtual Web application root and then insert it into an empty ASP.NET page by using a server-side include statement (see Listing 5.1).
Listing 5.1Including a Log File in an ASP.NET Page
<%@Page Language="VB" %> <html> <body> <pre> <!-- #include file="myappruntime.log" --> </pre> </body> </html>
Downsides of the Server-Side Include Technique
Although server-side includes are useful, there are at least a couple issues to be aware of with them. The first is one that has long annoyed users of classic ASP. The filename and path of the include file cannot be accessed or changed dynamically as the page executes. This is because the #include directive is processed before ASP.NET gets to see the page. You can't decide, for example, which file to include at runtime.
However, you can change the content of the section of the page that is generated from a server-side include file at runtime by including ASP.NET control declarations within the file and setting the properties of these controls at runtime. For example, if the include file contains the code shown in Listing 5.2, you can make the Webmaster's email address visible or hide it by setting the Visible property of the Panel control at runtime, as shown in Listing 5.3.
Listing 5.2Server-Side Include Files Containing ASP.NET Server Controls
©2004 Yoursite.com - no content reproduction without permission <asp:Panel id="WebmasterPanel" runat="server"> <a href="mailto:firstname.lastname@example.org">email@example.com</a> </asp:Panel>
Listing 5.3Setting Properties of Controls in a Server-Side Include File at Runtime
<!-- #include file="myfooter.txt" --> ... <script runat="server"> Sub Page_Load() If (some condition) Then WebmasterPanel.Visible = True Else WebmasterPanel.Visible = False End If End Sub </script>
When Is an Include File Actually Included?
Listings 5.2 and 5.3 prove that the include file is inserted into the page before ASP.NET gets to see it. The code in Listing 5.3 should produce a compile error and report that it can't find the control with ID WebmasterPanel because the declaration of this control is not in the page. However, by the time ASP.NET gets to compile the page, the include file has already been inserted into it.
Designer Support for Server-Side Include Files
The second issue with using server-side include files is that they are rarely supported in the tools that are available to help build pages and sites. This doesn't mean that you can't use them, but it does mean that you're unlikely to get WYSIWYG performance from the tool. However, this may not be important for things like footers or other minor sections of output.
ASP.NET User Controls
The server-side include approach we just discussed is useful and works well with ASP.NET. But there are other ways to build reusable content, and these techniques often overcome the limitations of server-side include files and also offer a better development model as a whole. The simplest, and yet extremely powerful, approach introduced with ASP.NET is the concept of user controls.
Whereas server-side include files are effectively just chunks of content that get inserted into the page before it is processed by ASP.NET, user controls are control objects in their own right. The System.Web.UI.UserControl class that is used to implement all user controls is descended from the same base class (System.Web.UI.Control) as all the server controls in ASP.NET.
This means that a user control is instantiated by ASP.NET and becomes part of the control tree for the page. It also means that it can implement and expose properties that can be accessed by other controls and by code written within the hosting page. And, because it is part of the control tree, any other server controls that it contains can also be accessed in code within the hosting page, as well as by code within the user control itself (see Figure 5.1).
Figure 5.1 Reusing user controls in multiple ASP.NET pages.
Registering and Inserting a User Control
A user control is written as a separate file that must have an .ascx file extension. It is then registered with any page that needs to use it, via the Register directive. The Register directive specifies the tag (element) prefix and name that will identify the user control within the page, and this prefix and name are then used to instantiate the user control at the required position within the declarative content of the page, as shown in Listing 5.4.
Listing 5.4Registering a User Control and Inserting It into a Page
<%@Page Language="VB" %> <%@Register TagPrefix="ahh" TagName="ComboBox" Src="ascx\combo.ascx" %> ... <body> Simple Combo List Box: <ahh:ComboBox id="cboTest1" IsDropDownCombo="False" runat="server" /> ... </body>
You can see in Listing 5.4 how similar the technique for using a user control is to using the standard server controls that are provided with ASP.NET. All the properties of the System.Web.UI.Control class are available (for example, id, EnableViewState, Visible) and can be set using attributes or at runtime in your code. The id property is set to "cboTest1" in Listing 5.4.
You can set the values of properties that are specific to this user control in exactly the same way. For example, Listing 5.4 shows the value of the IsDropDownCombo property being set to False. And any Public methods that the user control exposes can be executed from code in the hosting page, just as with a normal server control. Figure 5.2 shows a page that hosts the ComboBox user control you'll develop later in this chapter.
Figure 5.2 A ComboBox control implemented as a user control.
Running the ComboBox Control Example Online
If you want to try out this control, go to the sample pages for this book. You can also run it online on our own server, at http://www.daveandal.net/books/6744/combobox/combo.aspx.
The Contents of a User Control
As with server-side includes, you can place almost any content in a user control. It can be just declarative HTML or client-side code and text, or it can include ASP.NET server controls, server-side code, and even other user controls.
Nesting User Controls
Note that you can't insert an instance of the same user control into itself. The nested user control would then insert another instance of itself again, ad infinitum, creating a circular reference. The compiler would detect this situation and generate an error. If you need to nest user controls, you must create a hosting instance that references a different file that is identical in content except that it does not contain the reference to the nested control.
Oftentimes you need to insert the same user control more than once into a page, in the same way that you use server controls. Of course, this isn't obligatory, but it does mean that you need to bear in mind some obvious limitations to the content user controls include if you are to use them more than once. There are two things you should generally not include in a user control:
The opening and closing <html>, <title>, or <body> elements. These should be placed in the hosting page so that they occur only once.
Server-side form controls (for example, <form> elements that contain the runat="server" attribute). There can be only one server-side form on an ASP.NET page (except when you're using the MobilePage class to create pages suited to mobile devices).
A common scenario is to use a user control that generates no user interface (no visible output) but exposes code functions or subroutines that you want to be able to reuse in several pages. As long as these routines are marked as Public, they will be available to code running in the hosting pagewhich can reference them through the ID that is assigned to the user control. Listing 5.5 shows how you can access a method of a user control (which in this case just returns a value) and how you can set and read property values. Later in this chapter, you'll see in more detail how properties and methods are declared within a user control.
Listing 5.5Accessing Properties or Methods of a User Control
' call the ShowMembers method and get back a String Dim sSyntax As String = cboTest1.ShowMembers() ' set the width and number of rows of the control cboTest1.Width = 200 cboTest1.Rows = 10 ' read the current text value of the control Dim sValue As String = cboTest1.Text
User Controls and Output Caching
One extremely good reason for taking advantage of user controls (and, in fact, perhaps one of the prime reasons for their existence) is that they can be configured differently from the hosting page as far as the page-level directives are concerned. In an ASP.NET page, you can add a range of attributes to the Page directive and use other directives, such as OutputCache, to specify how the page should behave. This includes things like whether debugging and tracing are enabled, whether viewstate is supported, and how output caching should be carried out for the page.
The simplest output cache declaration specifies the number of seconds for which the output generated by ASP.NET for the page should be cached and reused, and it specifies which parameters sent to the page can differ to force a new copy to be generated. When you use an asterisk (*) for the VaryByParams attribute, a different copy of the page will be cached for each varying value sent in the Request collections (Form and QueryString):
<%@OutputCache Duration="300" VaryByParam="*" %>
Output caching provides a huge performance benefit when the content generated by the page is the same for most clients or when there are only a limited number of different versions of the page (in other words, when the values sent in the Form and QueryString collections fall into a reasonably small subset). When there are many different cached versions, the process tends to be self-defeating.
Managing Caching Individually for User Controls
User controls allow you to divide a page into sections and manage output caching individually for each section. This means that you can cache the output for sections that change rarely (or for which there are few different versions) for longer periods, while caching other sections for shorter periods or not at all.
The OutputCache directive can be declared in a user control, just as it can in a normal ASP.NET page, but it affects only the output generated by the user control. There is also one extra feature supported by the OutputCache directive when used in a user control: the Shared attribute.
User controls are designed to be instantiated within more than one ASP.NET page, and yet it's reasonable to suppose that the output they generate could be the same in many cases (regardless of the page that uses them). When the OutputCache directive in a user control includes the attribute Shared="True", the same cached output is used for all the pages that host this user control. This saves memory and processing when the output required is the same for all the pages that use the control.
The Downsides of User Controls
Although user controls provide a great development environment for reusable content, they also have a couple of downsides that you must consider. The first and most obvious of these is that they are specific to an ASP.NET application. Unlike the standard ASP.NET server controls, which can be used in any ASP.NET application on a server, user controls can only be instantiated in pages that reside in the same Web application (the root folder of the virtual application, as defined in Internet Services Manager, or a subfolder of this application that is not also defined as a virtual application).
In most cases, this is not a real problem. User controls tend to be specific to an application. For example, if you implement a footer section for all your pages as a user control, it probably makes sense for it to be used only within that application. However, some user controls (such as the ComboBox control shown earlier in this chapter) may be useful in many different applications. In this case, you will have to maintain multiple copies of the same user controlone for each application that requires it.
Furthermore, many people still tend to see user controls as being the "poor man's solution" for building controls, as in the ComboBox example earlier in this chapter. There are good reasons for this: One is that you can't expose events from a user control in the same way you can from a server control that is defined as a class and compiled into an assembly. We'll look at this topic in Chapter 8, "Building Adaptive Server Controls."
Finally, of course, you can't hide your code in a user control in quite the same way as you can by compiling a server control into an assembly. Like an ASP.NET page, the source of a user control is just a text file that must be present in the Web site folders. It's unlikely that you could build up your own software megacorporation just by selling user controls.
Custom Master Page and Templating Techniques
One common use of both server-side include files and user controls is to insert some common section of content into a page, perhaps to create the page header, the footer, or a navigation menu. There is, however, a technique that effectively tackles this issue from the opposite direction: You can create a master page or template for the site and base all the pages on this master page or template. All the content in the master page or template then appears on every page, and each individual page only has to implement the content sections that are specific to that page.
The master page approach tends to encompass the concept of the individual pages being dynamically generated each time from the master page, with the individual content sections being inserted into it (see Figure 5.3). However, bear in mind that ASP.NET pages are compiled on first hit and then cached, so the process happens only the first time the page is referenced and when the source of the page (the master page itself, or the individual content sections) changes.
Figure 5.3 Generating ASP.NET pages from a master page.
A template, on the other hand, usually conjures up a vision of a single page from which the individual content pages are generated in their entiretyrather like some kind of merge process (see Figure 5.4). In fact, using master pages and using templates are generically very similar, and both produce compiled pages that are cached for use in subsequent requests.
Figure 5.4 Generating ASP.NET pages from a template.
Chapter 9, "Master Pages, Templates, and Page Subclassing," looks at master pages and page templates; you'll see more discussion there of the different techniques you can use and the various ways you can code pages to provide the most efficient and extensible solutions.
ASP.NET Server Controls Built As .NET Assemblies
The next step up the ladder of complexity versus flexibility is to create reusable content as a native .NET server control. The controls you create using this technique are functionally equivalent, in terms of performance and usability, to the standard server controls provided with ASP.NET. The controls provided in the box with ASP.NET are written in C#, and they're compiled into assemblies. The ASP.NET Web Forms controls (those prefixed with asp:) are all implemented within the assembly named System.Web.dll, which is stored in your %windir%\Microsoft.NET\Framework\[version]\ folder.
Subsequent chapters show how easy it is to create your own server controls and then use them in Web pages just as you would the standard ASP.NET controls. Figure 5.5 shows the SpinBox control that is created in Chapter 8, with three instances inserted into the page and various styles applied to them.
Figure 5.5 A SpinBox control implemented as a .NET server control.
Server controls provide a few important advantages over user controls and most other reusable content methods. They encapsulate the code and logic, making it harder for others to steal any intellectual property they contain. Although server controls can still be disassembled to view the Microsoft Intermediate Language (MSIL) code they contain, most users are unlikely to be able to see how they work. You can also use obfuscation techniques (as built into Visual Studio) to make it much more difficult for even experienced users to discover the working of a control.
Second, user controls can expose events that you can handle in the hosting page, exactly as the standard ASP.NET Web Forms controls do. For example, the SpinBox control exposes an event named ValueChanged, which can be handled by assigning an event handler to the OnValueChanged attribute of the control, as shown in Listing 5.6.
Listing 5.6Handling the ValueChanged Event of the SpinBox Control
<ahh:StandardSpinBox id="spnTest1" runat="server" OnValueChanged="SpinValueChanged" /> ... ... Sub SpinValueChanged(sender As Object, e As EventArgs) ' display message when value of control has changed lblResult.Text &= "Detected ValueChanged event for control " _ & sender.ID & ". New value is " _ & sender.Value.ToString() End Sub
Third, server controls can be installed into the global application cache (GAC) so that they are available to all applications on the machine and not restricted to a single application, as are user controls and server-side include files. The following section looks at this particular topic in more detail.
Local and Machinewide Assembly Installation
In many cases, when you build custom controls as assemblies, you'll probably want to use them only within the ASP.NET application for which they were designed. As long as the assembly resides in the bin folder of the application, it will be available to any ASP.NET page (or Web service or other resource) that references it. All you need to do is add to the page an appropriate Register directive that specifies the tag prefix for elements that will declare instances of the control, the namespace in the assembly within which the control is declared, and the assembly filename, without the .dll extension:
<%@ Register TagPrefix="ahh" Namespace="Stonebroom" Assembly="std-spinbox" %>
You can then add an instance of the control to the page, using the following:
<ahh:SpinBox id="spnTest" runat="server" />
However, as just mentioned, you can make a control or an assembly available machinewide by installing it in the GAC. For a control to be available to all the applications on the machine, three major requirements must be met:
There must be a way for a control to be uniquely identifiable among all other controls, aside from its name. Because the assemblies that implement controls can be installed anywhere on the machine, the filename of the assembly is not sufficient to uniquely identify it.
There must be a way to specify the version of the control so that new versions can be installed for applications that require them, while the existing version can remain in use for other applications.
The .NET Framework requires that assemblies must be digitally signed using public key encryption techniques to protect the assemblies from malicious interference with the code.
You can meet all three of these requirements by applying a strong name to an assembly. You create a strong name by using a utility named sn.exe to generate a public encryption key pair, and then you add attributes to the assembly before it is compiled to attach this key pair to the assembly and specify the version, the culture, and optionally other information.
After the assembly has been compiled, you can add it to the GAC by using the gacutil.exe utility, the .NET Framework Configuration Wizard, or Windows Installer. Finally, ASP.NET pages that use the control must include a Register directive that specifies the assembly name, version, culture, and public key. For example, this is how you would register the version of the SpinBox control that is inserted into the GAC (and which has the name GACSpinBox):
<%@Register TagPrefix="ahh" Namespace="Stonebroom" Assembly="GACSpinBox,Version=18.104.22.168,Culture=neutral, PublicKeyToken=92b16615bf088252" %>
A Note About the Assembly Attribute
Important: The text string specified for the Assembly attribute of the Register directive must all be on one line and not broken as it is here due to the limitation of the page width.
In Chapter 8 you'll build the SpinBox server control you've seen in this chapter. At that point, you'll walk through the process, step-by-step, of making a server control globally available across applications.
The Downside of ASP.NET Server Controls
The only real limitation with building server controls is that you really have to know at least the basics of how your chosen language supports and implements features such as inheritance. You also need to understand the event sequence and the life cycle of controls. However, to quote that oft-used saying, "it's not rocket science." You can quickly pick up the knowledge you require.
Using COM or COM+ Components via COM Interop
Using components is a great way to provide encapsulated and reusable content, as you've seen in the preceding sections of this chapter. So far this chapter has talked about various types of components (using the word in the broadest sense) that are all fully compatible with ASP.NET. However, you may have COM or COM+ components that you are already using in a classic ASP application, or you might want to use COM components that are part of Windows or an application you have already installed in an ASP.NET application.
To use COM or COM+ components within the .NET Framework, you can create a wrapper that exposes the interface in a format that allows managed code to access it. You effectively create a .NET manifest that describes the component and that acts as a connector between the component and the .NET runtime environment. Each property, method, and event is mapped through the wrapper, and you can then use the component in the same way you would use a fully managed code (.NET) assembly.
The overall process is referred to as COM Interop, and it provides a path to move to .NET without having to rewrite all the business logic and custom components required in an existing or new application immediately, although you should consider this to be a temporary measure and aim to build native components as part of the process when and where possible.
Performance Issues with COM Interop
Using wrapped COM components affects the performance of your pages. The extra marshaling of values across the managed/unmanaged boundary with each property setting and method call is less efficient than with a native managed code component. The actual performance degradation generally depends on the number of calls you have to make when using the component; for example, a component that requires you to set a dozen property values and then call a method is likely to degrade performance more than one that lets you make a single method call with a dozen parameters. The actual marshaled size of the parameters or values you pass to properties and methods also has some effect on the performance.
Creating a .NET Wrapper for a COM or COM+ Component
If you are building an application by using Visual Studio .NET, you can create a type library wrapper by simply adding to your project a reference to the component. You right-click the References entry in the Solution Explorer window and select Add Reference. In the Add Reference dialog that appears, you go to the COM tab and select the component or library you want to use.
Alternatively, you can use the Type Library Import utility provided with the .NET Framework. The utility tlbimp.exe is installed by default in the Program Files\Microsoft.NET\SDK\[version]\Bin folder. To use it, you specify the COM component DLL name and add any options you want to control specific features of the wrapper that is created. You can find a full list of these options in the locally installed .NET SDK at ms-help://MS.NETFrameworkSDKv1.1/cptools/html/cpgrftypelibraryimportertlbimpexe.htm or by searching for tlbimp in the index.
Using the tlbimp Utility
As an example of how to use the Type Library Import utility provided with the .NET Framework, let's look at an example of how to create a wrapper for a fictional custom COM component. The DLL is named stnxsltr.dll, and it implements a class named XslTransform within the namespace Stonebroom. To create the wrapper, you would copy the DLL to a temporary folder and navigate to this folder in a command window. The following command runs the tlbimp utility for version 1.1 of the Framework and generates the type library wrapper as a .NET assembly with the .dll file extension:
"C:\Program Files\Microsoft.NET\SDK\v1.1\Bin\tlbimp" stnxsltr.dll
Notice in Figure 5.6 that the name of the new DLL is the name of the namespace declared within the component, not the filename of the original component DLL. This is required to allow ASP.NET to find the type library when it is imported into a page.
Figure 5.6 Executing the tlbimp utility to generate a wrapper for a COM component.
Now you would copy the new wrapper DLL into the bin folder of an application and use the component in ASP.NET pages just as you would a native .NET component. You'd use an Import directive to import the type library wrapper, and then instantiate the component by using the classname. You could use the full namespace.classname syntax when instantiating the component, but this is not actually required. Because the namespace has been imported, you could instantiate the component by using just the classname (see Listing 5.7).
Listing 5.7Using a Custom XslTransform COM Component in ASP.NET
<%@Import Namespace="Stonebroom" %> <script runat="server"> Sub Page_Load() Dim oXml As New XslTransform Dim sStatus As String Dim sXMLFile As String = "/data/xml/myfile.xml" Dim sXSLFile As String = "/data/xsl/myfile.xsl" Dim sOutFile As String = "/results/myfile.html" Dim blnWorked As Boolean = oXml.TransformXML(sXMLFile, _ sXSLFile, sOutFile, sStatus) lblResult.Text = sStatus End Sub
Coping with Classname Collisions
The fictional custom component described here has the same classname, XslTransform, as a native .NET class within the .NET Framework class library. However, you do not import the System.Xml.Xsl namespace (within which the .NET Framework component lives) into the page, so there is no collision of classnames. If there were, you would get a compilation error such as "XslTransform is ambiguous, imported from the namespaces or types System.Xml.Xsl, Stonebroom." In that case, you would use the full namespace.classname syntax to identify which class you require (for example, Dim oXml As New Stonebroom.XslTransform).
ASP Compatibility for Apartment-Threaded COM Components
When you're using COM or COM+ components, one issue to be aware of is that the threading model used in ASP.NET is not directly compatible with components that are single threaded or apartment threaded. Single-threaded components are not suitable for use in ASP or ASP.NET anyway, so this factor should not be an issue.
However, components built with Visual Basic 5 and 6 are usually apartment threaded (via the single-threaded apartment [STA] model) and work fine with only minor performance degradation in classic ASP. Until the arrival of the .NET Framework, which makes creating components in any managed code language easy, Visual Basic was quite a popular environment for building business components and server controls.
To overcome any issues with running apartment-threaded components in ASP.NET, you should always add the attribute ASPCompat="True" to the Page directive. This forces ASP.NET to adopt a threading model that matches the requirements of Visual Basic apartment-threaded components. It also allows components to access the intrinsic ASP objects, such as ObjectContext, and the OnStartPage method. There is some performance degradation, but it is not usually significant except in highly stressed Web applications and Web sites.
However, if you add the ASPCompat="True" attribute to a page that creates instances of apartment-threaded components before the request is scheduled, you will encounter much more significant performance degradation. You should always create instances of any apartment-threaded components you need in a Page event such as Page_Load or Page_Init.