Simulating include Files with an ASP.NET Server Control
Paul Sheriff
PDSA, Inc.
October 2003
Applies to:
Microsoft® ASP.NET
Microsoft Visual Studio® .NET
Summary: Learn how server controls have many benefits over user controls or simple #include files. Learn how to create a central location for HTML snippets and how to modify HTML for multiple sites in one location. (17 printed pages)
Download HTMLIncludeSample.msi.
The Scenario
Create Your Own Server "Include" Control
Build the Server Control
Test the PDSAInclude Control
Adding Caching Capabilities
Customizing your Web Custom Control
Summary
In many companies you will typically have multiple intranet and/or extranet sites. You will most likely wish to maintain a common look and feel across all of these sites. In Microsoft® ASP.NET there is no mechanism built-in to allow you to create HTML snippets and bring them in as a header or footer on each page across multiple Web sites (see Figure 1).
Figure 1. A central location for HTML is sometimes needed for enterprise development
For a single site you can use a user control, but then it must be copied to every other site whenever you make a change to the UI (see Figure 2). The reason for this lies in the fact that a user control must be compiled as part of the application under which it will run. If you make a change to one of the user controls on one of the Web sites, you will need to physically copy the .ASCX and associated .VB file to the other sites in your enterprise.
Figure 2. User controls are limited in that they can only be associated with one site.
In the past, when using Microsoft Active Server Pages, you would include common HTML snippets within each page using the #include directive. This directive would allow you to specify a file name to insert at the location where the #include was located within the ASP code. ASP.NET still supports the use of #include files, but there are a few issues that make their use unattractive.
- You must hard code the path on each page.
- There is no design-time interface to see what your page looks like.
- You must go into the HTML view of the page to enter the #include.
One way you can solve this problem is to create a server control that will read an HTML file from disk and display that HTML on any Web page within any Web site. The HTML file(s) can be centrally located where any Web site on any of your servers can get at them. In this way, you may modify the HTML files in one location and the change will affect all of your Web sites.
Figure 3. A server control can be used across multiple sites and bring in a centrally located HTML snippet.
In this article you will create a control named PDSAInclude. This server control will allow you to set a FileName property where the HTML you wish to render is located.
Like any server control, you will need to create some properties that you can set either at design time or at run time. For the PDSAInclude control you will need three different properties; FileName, Cache and HTML. The properties and their attributes are listed in Table 1.
Table 1. PDSAInclude properties
Property Name | Data Type | Description |
---|---|---|
FileName | String | The full path and file name of the HTML file. |
Cache | Boolean | True=Cache the HTML, False=Do not Cache the HTML |
HTML | String | The HTML read from the file. |
You will need some methods in the PDSAInclude control to perform the rendering of the control, reading the HTML from disk, and caching the HTML after you have read it. The methods you will create are listed in Table 2.
Table 2. PDSAInclude methods
Method Name | Type | Return Value | Description |
---|---|---|---|
Render | Protected Overrides Sub | n/a | Overrides the Render method for the base control. It is in this method that you will read the HTML file and display the HTML within this control. |
IsDesignMode | Private Function | Boolean | Return a True if the control is currently being displayed in the designer. Returns a False if the control is being rendered at runtime. |
GetHtml | Private Function | String | Attempts to open the file and read the HTML from the file. Places the HTML read into the HTML property of this control. The return value is the HTML read from the file. |
IsFileValid | Private Function | Boolean | Returns a True if the path and file input into the FileName properties exists, otherwise returns a False. |
CacheExists | Private Function | Boolean | Returns a True if the HTML from the file has already been cached in memory. |
GetFromCache | Private Function | String | Returns the HTML string from the Cache. |
SetCache | Private Sub | n/a | Places the HTML into the Cache. |
If you have never created a server control before, don't worry; the steps are fairly simple and it is just like creating any class library project. In general here are the steps you will perform.
- Create a Web Control Library project.
- Create a server control named PDSAInclude.
- Create an ASP.NET Web Application project named PDSAIncludeSample.
After you have completed these steps you will have a server control that can used across multiple Web sites.
To begin building the PDSAInclude server control you will need to first create a Web Control Library project. This project will contain just the one control, PDSAInclude. Follow the steps below to create this project.
- Start Microsoft Visual Studio® .NET and click the New Project button.
- Select Visual Basic Projects from the Project Types list
- Select Web Control Library from the Templates.
- Set the Name to PDSAInclude.
- Set the Location to any valid folder on your hard drive. For purposes of this article I will assume you place it into a folder called D:\MyControls.
- Click the OK button to create the new project.
Visual Studio .NET will create a new file named WebCustomControl1.vb that contains a template class with some attributes for your new custom control. You should see something like Listing 1 in your editor.
Listing 1. The default WebCustomControl class
Imports System.ComponentModel Imports System.Web.UI <DefaultProperty("Text"), _ ToolboxData("<{0}:WebCustomControl1 _ runat=server></{0}:WebCustomControl1>")> _ Public Class WebCustomControl1 Inherits System.Web.UI.WebControls.WebControl Dim _text As String <Bindable(True), Category("Appearance"), _ DefaultValue("")> Property [Text]() As String Get Return _text End Get Set(ByVal Value As String) _text = Value End Set End Property Protected Overrides Sub Render( _ ByVal output As System.Web.UI.HtmlTextWriter) output.Write([Text]) End Sub End Class
Unlike a user control, when you create a Web Custom Control, there is no design time interface. Web Custom Controls are built entirely from code. You will need to make a few changes to this standard template to create the PDSAInclude control. Start at the top of the new class file and make the following changes.
- Add
Imports System.IO
just below the otherImports
statements. - At the very top of the control, locate the DefaultProperty attribute and change it from
Text
toFileName
. - You will also modify the ToolboxData attribute and change it from
WebCustomControl1
toPDSAInclude
.
When you have completed these steps, the code at the top of the control should look like Listing 2.
Listing 2. Modify the attributes of the class to give this control a unique name
Imports System.IO <DefaultProperty("FileName"), _ ToolboxData("<{0}:PDSAInclude _ runat=server></{0}:PDSAInclude>")> _
Next you will need to change the class name and the file name.
Change the class name from WebCustomControl1 to PDSAInclude.
Change the file name from WebCustomControl1.vb to PDSAInclude.vb.
You will now add some private constants and variables to this control. These will be needed as we develop the properties for this server control. Just below theInherits
line in the class add the variables as shown in Listing 3.
Listing 3. Add private variables to hold the data for the properties of this class.
Private Const conHTML As String = "PDSAInclude" Private mstrFileName As String = "" Private mboolCache As Boolean = True Private mstrHTML As String = "" Private mstrMessage As String = "Fill in a File Name" Private mboolDesignMode As Boolean
At the same time you add these properties, delete the Text property declaration including the line of code that creates the _text variable. The lines to delete are shown in Listing 4.
Listing 4. Delete the Text property as it will not be needed for our application.
Dim _text As String <Bindable(True), Category("Appearance"), _ DefaultValue("")> Property [Text]() As String Get Return _text End Get Set(ByVal Value As String) _text = Value End Set End Property
You will now need to create the appropriateProperty
declarations that you will want to show up in the Properties window. Add the code shown in Listing 5.
Listing 5. Properties with the appropriate attributes will show up in the Properties window.
<Bindable(True), Category("Misc"), DefaultValue("")> _ Property FileName() As String Get Return mstrFileName End Get Set(ByVal Value As String) mstrFileName = Value End Set End Property <Category("Misc"), DefaultValue(True)> _ Property Cache() As Boolean Get Return mboolCache End Get Set(ByVal Value As Boolean) mboolCache = Value End Set End Property Property HTML() As String Get Return mstrHTML End Get Set(ByVal Value As String) mstrHTML = Value End Set End Property
Before you can render the HTML coming from an HTML file, you will first need to read that file from disk. Of course, you should also check to see if that file exists before you attempt to open it. As a result you will need to create a method within your control to see if the file in the FileName property does exist. Create the IsFileValid method just below the Render method in your PDSAInclude control. The code to write is shown in Listing 6.
Listing 6. The IsFileValid method will ensure the file exists prior to attempting to read it.
Private Function IsFileValid() As Boolean Dim boolReturn As Boolean If mstrFileName.Trim().Length = 0 Then mstrMessage = "Fill in a FileName" Else If File.Exists(mstrFileName) Then boolReturn = True Else mstrMessage = "File: " & mstrFileName & " does not exist" End If End If Return boolReturn End Function
The IsFileValid method uses the Exists method of the File class in the System.IO namespace to check for the existence of a file on disk. If the file does not exist, then you fill in the mstrMessage variable with an appropriate message that will be displayed on the Web page.
You now will write a method named GetHtml that will retrieve the HTML from the file you enter into the FileName property. Just below the Render event procedure add the GetHtml method as shown in Listing 7.
Listing 7. The GetHtml method reads the HTML from the file set in the FileName property.
Private Function GetHtml() As String Dim tr As System.IO.TextReader Try tr = File.OpenText(mstrFileName) mstrHTML = tr.ReadToEnd() tr.Close() Return mstrHTML Catch exp As Exception Throw exp End Try End Function
The GetHtml method uses the File class to perform its work. Using the OpenText method you will pass in the file name you set in the control's FileName property. The OpenText method will return a TextReader object. You then use the ReadToEnd method of the TextReader to read the complete file into the member variable mstrHTML. You then return this HTML from this method. The return value will be used in the Render event to output into the response stream.
The Render event is responsible for drawing the control both at design time and at runtime. The default Render event attempts to output the Text property. Since you have deleted this property you will need to modify this event. The purpose of this control is to render HTML read from a file. As a result you will need to write the appropriate code to read from a file and output that HTML in the Render event.
The Render event is passed in an HtmlTextWriter object from the page on which this control lives. This object is used to output HTML into the response stream. As an ASPX Web page renders itself, it will invoke the Render event of each control on the page and pass in that page's HtmlTextWriter to each control. Your job in this control is to read the HTML from file passed into the FileName property and output that HTML into the response stream.
Now modify the Render event procedure and modify the code to look like the code shown in Listing 8.
Listing 8. The Render event procedure will write the HTML to the Web page.
Protected Overrides Sub Render(ByVal output As _ System.Web.UI.HtmlTextWriter) Try If IsFileValid() Then output.Write(GetHTML()) Else output.Write(mstrMessage) End If Catch exp As Exception output.Write(exp.Message) End Try End Sub
You are now ready to give this control a try and see if it works. Follow the instructions in the next section to test this control.
To test out your new PDSAInclude Control, you will need to create a new ASP.NET Web Application project. You will then be able to add your new custom control to the Toolbox and drop it onto a Web page.
Click on the Solution PDSAInclude in the Solution Explorer Window.
Note Make sure you click on the Solution and not the Project.
Right-click on the Solution and select Add | New Project from the context menu.
Select ASP.NET Web Application from the list of templates.
Set the name of the project to PDSAIncludeSample.
Right-click on this new project and select Set as Startup Project from the context menu.
Right-click on the Toolbox and select Customize Toolbox from the context menu.
Click the .NET Framework Components tab.
Click the Browse button.
Navigate to the D:\MyControls\PDSAInclude\bin folder (or wherever you placed your control) and select the PDSAInclude.dll file.
Click the Open button. You should now see the PDSAInclude control appear in the toolbox.
Double-click on this new control and it should now draw the new control on the Web page.
Create an HTML page that you can read into your PDSAInclude control.
Using Microsoft Notepad or another editor, create a file in D:\MyControls named Header.htm.
Within this file place the following HTML snippet:
<H3>This is a header</H3>
Return to your Visual Studio .NET project and give the PDSAInclude control focus by clicking on it once.
Bring up the Properties window and locate the FileName property.
Set the FileName property to D:\MyControls\Header.htm.
Press the ENTER key.
As soon as you press the ENTER key you should see the HTML you put into your Header.htm file appear where the control is located on the Web page. If you run the Web page this HTML will also appear in the running page.
Instead of reading HTML from disk every time you render your PDSAInclude control, you could add caching capabilities into your control. The first time you read from the disk, you store the HTML into some sort of cache. Then the next time your control is rendered, the HTML is read from the cache instead of disk (see Figure 4).
Figure 4. Store commonly used HTML snippets in a cache for better performance.
Where you store the HTML is up to you. You could use a Cache object, the Application object, or even a Session object. In this article you will use the Application object.
The first method you need to create will be SetCache. This method will place the HTML into the Application object. For the name of the Application variable, you will combine the constant that you created at the beginning of this article, conHTML
, and the name of the file. Listing 9 shows an example of the SetCache method. Add this method to your control now.
Listing 9. The SetCache method will store the HTML into an Application object.
Private Sub SetCache(ByVal Value As String) Try HttpContext.Current.Application.Lock() HttpContext.Current.Application(conHTML & FileName) = Value HttpContext.Current.Application.UnLock() Catch Throw End Try End Sub
This method will need to be called from the GetHtml method. Modify the GetHtml method so it looks like Listing 10.
Listing 10. The GetHtml method will cache the results after reading from the disk the first time.
Private Function GetHTML() As String Dim tr As System.IO.TextReader Try tr = File.OpenText(mstrFileName) mstrHTML = tr.ReadToEnd() tr.Close() If Not mboolDesignMode Then ' Cache the results by file name If mboolCache Then SetCache(mstrHTML) End If End If Return mstrHTML Catch exp As Exception Throw exp End Try End Function
You will notice that that the GetHtml method now uses the mboolDesignMode variable. This Boolean variable will be set in a method called IsDesignMode. This method is needed so you do not try to retrieve HTML from the Cache at design time. The Application object does not exist during design time, so we need to set this variable so our code always gets the HTML from disk during design mode. Listing 11 shows the IsDesignMode method. Add this method to your control at this time.
Listing 11. The IsDesignMode method checks to see if the control is running or not.
Private Function IsDesignMode() As Boolean Try If HttpContext.Current Is Nothing Then mboolDesignMode = True Else mboolDesignMode = False End If Return mboolDesignMode Catch Throw End Try End Function
The easiest method to check if the control is at design mode is simply to check the current HttpContext. If this variable returns Nothing, then the control is in design mode.
In the Render event you will want to check if there is data in the Cache prior to getting the HTML from disk. To do this, you simply need to check if the Application variable where you stored the HTML exists already. This is accomplished by creating a method named CacheExists as shown in Listing 12.
Listing 12. Check the Cache before trying to retrieve HTML.
Private Function CacheExists() As Boolean If HttpContext.Current.Application(conHTML & FileName) _ Is Nothing Then Return False Else Return True End If End Function
If the CacheExists method returns a True
, then you need to call a method to retrieve the HTML from the cache. Create a method GetFromCache as shown in Listing 13.
Listing 13. Get the HTML from the Cache
Private Function GetFromCache() As String Try Return HttpContext.Current.Application(conHTML & FileName).ToString() Catch Throw End Try End Function
Now that you have created all the methods related to retrieving data from a cache, you are now ready to use these methods. You will now modify the Render event to check if the control is in design mode. If it is, then you will get the HTML from the file. If the control is running, you will first check to see if the HTML has already been cached. If it has you will call the GetFromCache method. If it is not already in the Cache, you will then retrieve the HTML from disk. Modify the Render event by typing the code shown in bold in Listing 14.
Listing 14. Modify the Render event to check the cache.
Protected Overrides Sub Render(ByVal output As System.Web.UI.HtmlTextWriter)
Try
If IsFileValid() Then
If IsDesignMode() Then
' We are in design mode
output.Write(GetHTML())
Else
' See if we can get data from cache
If Me.CacheExists() Then
output.Write(GetFromCache())
Else
output.Write(GetHTML())
End If
End If
Else
output.Write(mstrMessage)
End If
Catch exp As Exception
output.Write(exp.Message)
End Try
End Sub
To finish your custom control you will probably want to supply a custom bitmap to appear in the toolbox. In addition the TagPrefix for these custom controls is currently set to "cc1", and you might wish to supply a better prefix for all of your own custom controls.
To change the toolbox bitmap you will first have to create a bitmap. There are a few requirements that must be met to change the bitmap that appears in the toolbox.
- The bitmap must be a 16 x 16 pixel, 16-bit color bitmap, saved as a .BMP file.
- It must have the same file name as your control's class name. Since the name of your custom control is PDSAInclude, you will need to save your image as PDSAInclude.bmp.
- You will need to change the Build Action property of the bitmap control to Embedded Resource. This informs Visual Studio to compile the bitmap into the project.
Follow the steps below to create a bitmap for your control:
- Select Project | Add New Item from the Visual Studio.NET menu.
- Select Bitmap File from the list of templates.
- Set the name to PDSAInclude.
- Click on the new bitmap and set the Build Action property to Embedded Resource.
- Draw something.
- Build the Project.
- Switch back to the PDSAIncludeSample project and open the WebForm1.aspx page in design mode.
- Right-click on the PDSAInclude control in the toolbox and select Delete from the context menu.
- Re-add the PDSAInclude control back to the toolbox to see the new bitmap you just created.
If you were to look at the HTML view in your WebForm1.aspx page you will see a Register tag at the top. This tag informs the .ASPX page where the definition for this control comes from. If you look at this directive right now, you should see something that looks like the following:
<%@ Register TagPrefix="cc1" Namespace="PDSAInclude" Assembly="PDSAInclude" %>
If you then look further down within the<FORM>
tag, you will see the definition for your PDSAInclude control. You will see the cc1
prefix that matches the TagPrefix in the Register directive.
<cc1:PDSAInclude id="PDSAInclude1" runat="server">
</cc1:PDSAInclude>
You will most likely wish to change this prefix as "cc1" is not very descriptive. To accomplish this you will need to follow the steps below.
Double-click on the AssemblyInfo.vb file within the PDSAInclude project.
Add an
Imports
statement at the top of the file to bring in the System.Web.UI namespace:Imports System.Web.UI
Locate the
<Assembly:>
tags within this file and add the following tag:<Assembly: TagPrefix("PDSAInclude", "pdi")>
Rebuild your solution.
You must now delete the control from the toolbox and re-add the control to the toolbox to have it refresh the TagPrefix attribute. You will also need to delete the control from your Web page, and you will most likely have to manually delete the old Register directive from the HTML view of the WebForm1.aspx file.
After adding the control back to your Web page you can now view the HTML and see the new TagPrefix of "pdi"
<%@ Register TagPrefix="pdi" Namespace="PDSAInclude" Assembly="PDSAInclude" %>
You will also see the new prefix in the declaration of your custom control as well.
<pdi:PDSAInclude id="PDSAInclude1" runat="server"></pdi:PDSAInclude>
Of course when you delete the control, all of the design time properties have been deleted as well. You will need to reset the FileName property.
Server controls have many benefits over user controls or simple #include files. Some of the best benefits include: getting a design time rendering of the control, and the ability to centralize the code. With user controls you get no rendering and you must copy the control from one site to another. The controls presented in this article will provide you the ability to have a central location for all your HTML snippets like headers and footers for your Web sites.
Paul D. Sheriff is the President of PDSA, Inc., a Microsoft Partner in Southern California. Paul is the Microsoft Regional Director for Southern California, and he has three books on .NET. The first book is entitled "ASP.NET Developer's Jumpstart" from Addison-Wesley and is co-written with Ken Getz. His other two books are eBooks and can be purchased directly from the PDSA Web site. You can contact Paul directly at PSheriff@pdsa.com.