Article Author: Benton Stark
Introduction
The benefits of web based applications are numerous but so are the design requirements. The "read-only" page is one basic requirement you may face when developing your next web application. A typical application contains many editable data entry pages. As data is entered and stored, the application may need to protect changes to the data based on who the user is. Different users require different access permissions to the data. For example, you may need to prevent the customer service representative from editing data in a billing application. But, the customer service representative needs a non-editable view of billing information to perform their duties. Another example is a document centric application that goes through various stages of completion and review. You may be developing an internal mortgage application system with many pages that requires extensive amounts of data to be entered by the applicant or loan officer. After the mortgage application is complete, the document needs to be protected. Transforming the page into a read-only document protects the mortgage application from unauthorized changes. Finally, a common example is a user confirmation page that you might generate before final submission of data. When the user clicks confirm you show a read-only view of the page. This allows the user to review the information entered and then decide whether to finally submit the data or to go back and review.
Using the power of ASP.NET and dynamic controls, discover how to iterate the Page.Controls collection and dynamically replace editable controls such as the TextBox , CheckBox , DropDownList , RadioButtonList , and CheckBoxList with the non-editable LiteralControl . The result is a very fast read-only web page saving you many hours of development time and effort.
System Requirements
To run the code for this sample you should have
Installing and Compiling the Sample Code
The C# sample download for this article contains the VS.NET the project ReadOnlyDemo.csproj . This sample code demonstrates the use of dynamic controls to create a read-only page using various common input controls.
To install the sample, you should first create an IIS virtual directory called ReadOnlyDemo , which points to the folder to which you have installed the project.
Four Common Solutions
Creating a read-only page is a common requirement for many web applications and there are several different approaches to solve this problem. Therefore, I have identified four typical solutions and described each approach in detail. They are the "Slap on a Label Control", the "Disappearing Button", "TextBox ReadOnly Property", and the "Duplicate Page" solutions. The Fifth solution is the subject of this article and solves the read-only page problem using dynamic controls.
Slap on a Label Control
I have personally used this technique in the past. As the developer, you simply add a Label control next to every type of input control. You must add at least one line of code for each Label control to set the Text property. An example of the technique is shown in Figure 1. The CheckBoxList , RadioButtonList , and other types of controls require additional lines of code to display all the options selected. With all these Label controls, the form designer becomes quickly cluttered. And, adding hundreds of Label controls along with the supporting code such as setting visible = false is a very error prone and time consuming task.

Figure 1. Design view of Label controls inserted next to TextBox Controls
Disappearing Buttons
This solution, if you can call it that, simply hides the update or other action buttons from the user. Unfortunately, the user is left utterly confused. The user (victim) will scour the web page looking for an Update or Save button. After all, the user thinks, why would the programmer let me edit all this data and give no method to save it? This solution is fast to implement but not intuitive for the user. In fact, your email inbox will probably begin filling up with requests for you to fix your "bug" if you use this technique.
TextBox ReadOnly Property
At first glance, this technique appears perfect. I thought so when I first encountered it. When you want to make the TextBox controls on a page read-only, you simple set the ReadOnly property to true . ASP.NET will inject the proper readonly attribute into the correct location. Then, the user’s browser will do the rest to make the input control read-only and protect your data from editing. Unfortunately, this technique only works with the TextBox control. This is because HTML 4.0 supports the readonly attribute for the < INPUT> and <TEXTAREA> elements only. In addition, this method has another serious flaw. Once a TextBox is read-only, the user can not view textual content beyond the width of the TextBox . This is particularly frustrating if the control is a multi-line TextBox . An example of a TextBox with the ReadOnly property set to true is shown in Figure 2(notice the whole email address is not visible and the user will not be able to scroll to view it.)

Figure 2. TextBox where the contents are beyond the view of the control and the control can not be scrolled
Duplicate Page
In this solution you create a separate page almost identical to the original. Yet, the second page is made up of Label controls or basic HTML and therefore can not be updated. This technique is very flexible since you can design your read-only page independent of the data entry page. But, it requires a lot of additional coding and future changes must be implemented in two places instead of one. Another variant of this type of page is to create read only documents using formats other than HTML. For example, a site might render a read only PDF documents that can be viewed and printed by the user. Generally, third-party web server controls or a reporting engine solution are required to produce PDF documents in ASP.NET.
A Fifth Solution using Dynamic Controls
ASP.NET gives you the amazing power to programmatically manipulate the construction of your web page. Each Page contains a Controls collection arranged in a parent-child relationship that can be accessed though the Page.Controls property. This hierarchical nesting is how ASP.NET keeps up with the physical placement of the controls on your page as well as the relationship between those controls. For example, HTMLForm has a Controls collection that contains all the controls owned by the HTMLForm . In addition, HTMLForm is a child of Page . See Figure 5 later in this article for an example of the layout of the controls in the demonstration project.
This article will show you how to recursively iterate though the Controls collection and locate every input control on your page – even those that are nested in a parent-child relationship. When a specific control type is found you will learn how to dynamically replace the control with a LiteralControl . The LiteralControl is a light weight web server control for displaying literal text in an HTML page. The difference between a LiteralControl and a Label is that the LiteralControl does not contain code the code which allows for server side processing such as formatting of HTML text using fonts and cascading style sheets. This control is designed to simply emit text into the page as it is being created and nothing more.
Using dynamic controls requires minimum time to code and implement. Even nested Repeater controls and Grid controls with templates are handled properly. In addition, future design changes to the page do not impact your read-only logic. New controls can be added without the necessity of additional read-only code. Finally, and most importantly, you can deliver required functionality on-time and with little coding effort. Demonstration
The demonstration code that you can download for this article illustrates the replacement of the following input controls.
- TextBox (single or multi-line)
- CheckBox
- CheckBoxList
- RadioButtonList
When you run the ReadOnlyDemo and view the ContactInfo.aspx page, you will be presented with a data entry page as show in Figure 3. This page represents a simple personal contact information page. It contains a variety of input controls as well as a Repeater control at the bottom.

Figure 3. Normal user editable view of the Contact Information page
As you can see, there is only one button on the page. When you click the Make Read-only button, the page is posted back to the server and the Make Read-only button’s click event is invoked. Next, the method ReadOnlyUtils.ReplaceControls() static method is called. This method does all the hard work. It iterates though the Page Controls collection replacing each input control with a LiteralControl . The end result is the read-only page as shown in Figure 4. Notice how Preferred Contact Method and Category now only show the item that was selected. Also note that Outdoor Activities is turned into a list of select items separated by commas. Each type of control is handled differently depending on the control’s characteristics. The result is an easy to read textual representation of the user’s selections.

Figure 4. Read-only version of the Contact Information page
Show Me the Code!
We begin the page transformation process by invoking the static method ReplaceControls() on the ReadOnlyUtils class. Because the ReplaceControls() method is called frequently and is used only once per page, I use a static method to simplify my code. Using a static method eliminates the need to create an instance of the ReadOnlyUtils object thereby makes the code much cleaner and easier to use.
private void btnMakeReadOnly_Click(object sender, System.EventArgs e)
{ // call the readonly page locking method ReadOnlyUtils.ReplaceControls(this.Controls); …
}
The ReplaceControls() method receives a reference to the Controls collection on the Page object via the reference this.Controls . It then iterates through each control using a for loop and tests the object type using the is operator.
public static void ReplaceControls(ControlCollection controls)
{ // an enumerator can only be used if the // collection is not altered – so I use a for loop instead for (int index = 0; index < controls.Count; index++) { Control ctrl = controls[index]; if (ctrl.Visible true) { if (ctrl is TextBox) ReplaceTextBox(index, (TextBox)ctrl); else if (ctrl is DropDownList | ctrl is RadioButtonList | ctrl is ListBox) ReplaceListControl(index, (ListControl)ctrl); else if (ctrl is CheckBox) ReplaceCheckBox(index, (CheckBox)ctrl); else if (ctrl is CheckBoxList) ReplaceCheckBoxList(index, (CheckBoxList)ctrl); } // recursive call to walk child collections if (ctrl.Controls.Count > 0) ReplaceControls(ctrl.Controls); } }
ReplaceControls() is looking for any objects of type TextBox , DropDownList , RadioButtonList , CheckBox , or CheckBoxList . For example, if the control is of type TextBox then ReplaceTextBox() is called. The current index and a reference to the TextBox control are passed as parameters to the function. Notice the ctrl object is cast to a TextBox object on the call to ReplaceTextBox() . This casting is necessary so that we can pass the control object as the type TextBox . The TextBox control inherits from the Control object.
Replacing the TextBox Control
The first line of code in ReplaceTextBox() simply creates a new LiteralControl used to replace the TextBox . The TextBox replacement works for both single and multi-line TextBox objects. Next, the Text property of the LiteralControl is set to the same value as the Text property of the TextBox control. Then, a new ID value for the LiteralControl is created by appending the string "_readonly" to the original ID value. Later in the article I will show you how to find the new dynamic controls in the Control Tree using a page trace. The Controls collection requires a unique ID value. If you do not set the ID for the dynamic control, the Controls collection will create a generic value for you. Next, the newLiteral object is added to the Controls collection. Notice that the collection resides on the parent object of the TextBox control. We access this collection using the Parent property. The Parent property is a reference back to the object that "owns" or contains the origTextBox object in the control hierarchy. This property returns a reference to the server control's parent control in the page control hierarchy. Having the ability to reference the parent from the child is a very powerful feature of the Controls collection.
First, the original TextBox control is removed from the parent's Controls collection using the RemoveAt() method and specifying the current index. Next, the new control is added to the parent's Controls collection using the AddAt() method with the same index. Now, I have effectively replaced the original TextBox control with a new LiteralControl . If the TextBox control is not removed, the Viewstate will become invalid on a post back and ASP.NET will throw an error complaining that the Viewstate and the Page Controls collection are no longer synchronized.
private static void ReplaceTextBox(int index, TextBox origTextBox)
{
LiteralControl newLiteral = new LiteralControl();
newLiteral.Text = origTextBox.Text;
newLiteral.ID = origTextBox.ID + "_readonly";
ControlCollection parentCol = origTextBox.Parent.Controls;
parentCol.RemoveAt(index);
parentCol.AddAt(index, newLiteral);
}
Replacing the CheckBox Control
The replacement for a CheckBox is very similar to a TextBox . If ctrl in ReplaceControls() is of type CheckBox , then ReplaceCheckBox() is called. The CheckBox object is a boolean type of input control. Therefore, the Checked property is inspected. If the Checked property is true then I set the LiteralControl.Text property to the string value Yes . If the Checked property is false then I set the LiteralControl.Text property to the string value No . The next two steps are the same as replacing a TextBox control. As before, the newLiteral control is added to the parent's Controls collection using the AddAt() method after the origCheckBox is removed from the parent's Controls collection.
private static void ReplaceCheckBox(int index, CheckBox origCheckBox)
{
LiteralControl newLiteral = new LiteralControl();
if (origCheckBox.Checked true)
newLiteral.Text = "Yes";
else
newLiteral.Text = "No";
newLiteral.ID = origCheckBox.ID + "_readonly";
ControlCollection parentCol = origCheckBox.Parent.Controls;
parentCol.RemoveAt(index);
parentCol.AddAt(index, newLiteral);
}
Replacing the CheckBoxList Controls
The CheckBoxList is replaced in much the same way as the TextBox and CheckBox controls with one exception. In ReplaceCheckBoxList() the selected options for the CheckBoxList are placed together in a single String separated by a comma and space. The Items property in the CheckBoxList is looped through using a foreach statement. If the item is selected, the Text property for that item is appended to the StringBuilder object sBuilder . After the loop is complete, I remove the last comma and space in the string list for formatting purposes. Now, the newLiteral.Text is set to the sBuilder.toString() value. The final two steps are the same as replacing a TextBox control. Finally, newLiteral is added to parent’s Controls using the AddAt() method after origCheckBoxList is removed from the parent’s Controls collection.
private static void ReplaceCheckBoxList( int index, CheckBoxList origCheckBoxList)
{ LiteralControl newLiteral = new LiteralControl(); StringBuilder sBuilder = new StringBuilder(); foreach (ListItem item in origCheckBoxList.Items) { if (item.Selected true) { sBuilder.Append(item.Text); sBuilder.Append(", "); } } string selections = sBuilder.ToString(); if (selections.Length > 0) newLiteral.Text = selections.Substring(0, selections.Length - 2); newLiteral.ID = origCheckBoxList.ID + "_readonly"; ControlCollection parentCol = origCheckBoxList.Parent.Controls; parentCol.RemoveAt(index); parentCol.AddAt(index, newLiteral); }
Replacing the DropDownList and RadioButtonList
The DropDownList, RadioButtonList and ListBox controls are replaced in a combined function. Since these controls inherent from the same base class ListControl , I take advantage of polymorphism and handle both class types with a single function. In the function ReplaceListControl() the second parameter is of type ListControl . The SelectedItem of origListcontrol contains the textual description of either the selected radio button item or the current drop down list selection on the page. Now, the newLiteral.Text is set to the origListcontrol.SelectedItem.Text value. The final two steps are the same as replacing any other control. Finally, newLiteral is added to the parent's Controls using the AddAt() method after the origListControl is removed from the parent's Controls collection.
private static void ReplaceListControl(int index, ListControl
origListControl)
{
LiteralControl newLiteral = new LiteralControl();
if (origListControl.SelectedItem != null)
newLiteral.Text = origListControl.SelectedItem.Text;
newLiteral.ID = origListControl.ID + "_readonly";
ControlCollection parentCol = origListControl.Parent.Controls;
parentCol.RemoveAt(index);
parentCol.AddAt(index, newLiteral);
}
Recursive Call
One of the more interesting lines of code in ReplaceControls() is a recursive call at the end of the code block. Recursion is defined as a programming technique whereby a function or method calls itself one or more times Recursion is a very useful programming technique for traversing linked collections or tree type structures where the end point is not known ahead of time and the linking is dynamic. For example, a recursive function would work well when searching for a file deeply embedded in some folder on your hard drive. In this case, the end point is when there are no more subfolders to search in a particular folder. Imagine trying to write a program that searches your entire hard drive and all the folders that are on it. To do this, you must locate all the subfolders and search them and then locate the subfolder's subfolders and so on. Recursion programming greatly simplifies this task.
Each control on your page belongs to a Controls collection. To enable the creation of the Controls hierarchy, other controls can have their own Controls collection as well. In fact, every control belongs to some parent object. For example, the TextBox controls containing phone number information are children of the Repeater control in Figure 3. Using recursion, you can traverse down all the various hierarchical pathways of the nested Controls collections.
// recursive call to walk children collections if (ctrl.Controls.Count > 0) ReplaceControls(ctrl.Controls);
As you can see, ReplaceControls() actually calls itself if there are additional controls to replace ( ctrl.Controls.Count > 0 ) nested in the current ctrl object. When searching the Controls hierarchy, you follow a pathway of nested controls by calling ReplaceControls() until you reach the end point ( ctrl.Controls.Count 0 ). Then the execution returns down the same execution path but now in reverse order. Try placing a break on the recursive call and stepping through the code to get a better understanding of recursion and what happens when the track runs out and you have to come back to the calling function. Don’t worry if you may feel a little dizzy after stepping though the all the recursive calls. Recursion can be a little confusing the first time you try to decipher it.
Tracing the Control Tree
In order to see the underlying effects of what the code does you can turn tracing on within the Control Tree. The output shows a hierarchical view of the controls with the Page object as the topmost root of the tree. By adding the @Page directive trace=true you can view the Controls collection in action. Figure 5 is a view of the trace output generated by ASP.NET. The output shows a hierarchical view of the controls with the Page object as the topmost root of the tree. Figure 6 is a view of the trace output after the page controls are replaced with the dynamic LiteralControl s.
As you can see, each dynamic control ends in "_readonly." Look for the controls by the new extensions that were added to the ID properties. Also notice that all the "_readonly" named controls such as tbFirstName_readonly are of type System.Web.UI.LiteralControl . You can use the trace functionality to give you an inside view of how your controls are being replaced in the Controls collection.

Figure 5. Control Tree view in trace output

Figure 6. Control tree view in trace output after replacement with dynamic controls
Viewstate Limitation
There is one basic limitation that you should be aware of when using this technique in your applications. Since dynamic controls are being added to the Page Controls collection, ASP.NET does not update the page Viewstate for these dynamic controls. Therefore, if you do a post back (for example a button click event), your page will lose most of the dynamic control information. This occurs because the dynamic controls Viewstate is no longer matching up with the original controls. To work around this problem, you must rebuild the page and then re-run the ReplaceControls () method during every post back.
Further Work
To make this code work with all your applications, you may need to handle the replacement of additional control objects. Various third party controls can be tested and replaced according to their behavior and functionality. In addition, you might want to automate the hiding of various button controls on your pages.
Other work includes manipulation of the Viewstate to preserve the LiteralControl values in subsequent post backs. Since the LiteralControl does not support Viewstate you will need to use another dynamic object such as the Label or Literal object when doing your replacement. In order to make this work you will probably have to override the rendering behavior on the page and load your own version of the viewstate.
Conclusion
Read-only pages are a standard requirement for many web applications. You learned about the four basic approaches in addressing the read-only page issue. Then, I showed you how to iterate through the Page Controls collections using recursion to perform the steps that dynamically created a read-only page. The power of .NET and dynamic controls allows you to manipulate your web pages in a programmatic manner. Using this technique, you can create unique solutions to solve many recurring problems in code. In this solution, I used dynamic controls to redefine the interface of the web page.
Dynamic controls have saved me countless hours of development time and made my application code much easier to maintain and extend. Most importantly, it empowered me to implement required functionality on time and within my client’s budget. Learn how to take advantage of dynamic controls and manipulate your pages to save time, money, and headache.

