In Listing 12.1, we saw a very simple custom DataGrid column class that displayed the message "Hello, World!" in each row. It accomplished this by overriding the DataGridColumn class's InitializeCell() method, setting the Text property of the passed-in TableCell to "Hello, World!" The code for Listing 12.1's InitializeCell() is overly simple, and such simple syntax, unfortunately, cannot be used for most custom DataGrid column classes. To illustrate why the InitializeCell() syntax from Listing 12.1 can only be used in the simplest of cases, let's step through the creation of a custom DataGrid column class that censors profanity from the values it displays. For example, in previous chapters we examined displaying the contents of a Comments table that contains comments from an online guestbook. Clearly, these entries could contain offensive words. If we are using a DataGrid to display the contents of the online guestbook, we can display the database fields that might contain offensive material using a censor DataGrid column. That is, we can display our guestbook using the following DataGrid syntax: <asp:DataGrid runat="server" ...> <Columns> <asp:BoundColumn DataField="Name" HeaderText="Poster" /> <custCols:CensorColumn DataField="Comment" HeaderText="Comment" /> </Columns> </asp:DataGrid> The DataGrid specifies that two columns should be displayed: a standard BoundColumn column that displays the contents of the Name field, and a CensorColumn column that lists the contents of the Comment field, censoring the contents. If your DataGrid column class needs to reference data from the DataGrid's DataSource, you can only access such information in the DataBinding event handler, not in the InitializeCell() method. Figure 12.3 shows a screenshot of the CensorColumn in action. Figure 12.3. The CensorColumn displays the Comment field, censoring profane or offensive words. Now that we understand the functionality of the CensorColumn column class, let's create this class. Clearly, the CensorColumn class behaves a lot like the BoundColumn class it displays contents from a database field and applies stylistic formatting to the results. Therefore, it would make sense to derive the CensorColumn class from the BoundColumn class, much like the LimitColumn class in Listing 12.3. However, let's take the nonsensical route and derive the CensorColumn class from the DataGridColumn class so that we can get a better understanding of what code needs to be provided in the InitializeCell() method to enable the contents of a DataGrid to be dependent upon the DataGrid's DataSource contents. NOTE Examining how to perform data binding from a class derived from the DataGridColumn class will also prove useful when examining more complex custom DataGrid column class examples. Additionally, it will give us insight as to how the BoundColumn class is implemented. Displaying a DataSource Field in a Custom DataGrid Column ClassThe BoundColumn class essentially accesses the DataSource field specified by its DataField property, and sets the TableCell's Text property to this DataSource field. Our CensorColumn class will essentially need to perform these same steps, but after it has the proper DataSource field, it needs to first censor the profane words from it (if any exist), and then have it displayed in the TableCell. To accomplish this, our CensorColumn class will need a DataField property just like the BoundColumn class. Implementing the code to retrieve the proper field from the DataSource is a bit involved. Before we examine the code to accomplish this, let's first look at the shell of the CensorColumn class, as shown in Listing 12.5. The CensorColumn class is derived from the DataGridColumn class (line 7), and contains a DataField string property (lines 15 24). Beginning on line 9 is the overridden InitializeCell() method, which is called once for every row added to the column. Listing 12.5 The CensorColumn Class Is Derived from the DataGridColumn Class[View full width] 1: Imports System 2: Imports System.Web.UI.WebControls 3: Imports System.Web.UI 4: Imports System.Web 5: 6: Public Class CensorColumn 7: Inherits DataGridColumn 8: 9: Public Overrides Sub InitializeCell(ByVal cell As TableCell, ByVal columnIndex As Now that we have the shell of the CensorColumn class in place, we can examine in detail the code needed to retrieve the proper DataSource field. Recall from Chapter 2, "Binding Data to the Data Controls," that the DataGridItem class has a DataItem property. When the DataGrid's DataBind() method is called, the DataSource's contents are enumerated, and for each DataSource item, a new DataGridItem object is created and added to the DataGrid. At this time, the DataSource's current item is assigned to the newly created DataGridItem object's DataItem property. So, if the DataSource contains the results of a SQL query, the DataItem will contain a particular row from the query results. Using data-binding syntax in a TemplateColumn, we can reference a particular field of the row using DataBinder.Eval(Container.DataItem, "FieldName") When such syntax appears within a TemplateColumn, we are dealing with a specific cell in the DataGridItem, so to reference the DataGridItem's DataItem property, we have to use Container.DataItem. To reference a particular field in the DataSource in our CensorColumn, we must use a similar approach, referencing the DataGridItem's DataItem property. As with the data-binding syntax in the TemplateColumn, the TableCell cell in the CensorColumn class's InitializeCell() method is contained by the DataGridItem. Hence, we first have to reference the DataGridItem object that contains cell to get to the needed DataItem property. After the last few paragraphs, I don't blame you if you're very confused! This is tricky subject matter. Let's look at some code that should help clarify things. The following snippet of code illustrates how the DataItem property can be referenced by examining the container of the TableCell cell. 'Get the DataGridItem that contains the TableCell cell Dim gridItem as DataGridItem = cell.NamingContainer 'Get the DataItem property from gridItem Dim DataItem as Object = gridItem.DataItem 'Display a particular DataItem field in the contents of cell cell.Text = DataBinder.Eval(dataItem, "FieldName") These three, short lines of code accomplish quite a bit. The first line accesses the object that contains the TableCell cell. This is accomplished by referencing cell's NamingContainer property. (The NamingContainer property is defined in the Control class, meaning all Web controls, including TableCell, have this property.) Next, the DataItem property of the DataGridItem gridItem is referenced and stored in the local variable dataItem. Finally, the DataBinder.Eval method is used to retrieve a particular field from the dataItem object and assigns the value to cell's Text property. Unfortunately, the code just examined cannot be placed in the InitializeCell() method because when the InitializeCell() method is executed, the DataGridItem's DataItem property has yet to be set to the particular DataSource item. Rather, the code must be placed in the DataBinding event handler for the TableCell cell. Listing 12.6 provides the code for the CensorColumn class that correctly displays a DataSource field in the DataGrid column. Listing 12.6 The TableCell's Text Property Is Set in the TableCell's DataBinding Event Handler[View full width] 1: Imports System 2: Imports System.Web.UI.WebControls 3: Imports System.Web.UI 4: Imports System.Web 5: 6: Public Class CensorColumn 7: Inherits DataGridColumn 8: 9: Public Overrides Sub InitializeCell(ByVal cell As TableCell, ByVal columnIndex As The InitializeCell() method in Listing 12.6 (lines 9 16) checks to determine what type of cell is being added. In the event that an Item, AlternatingItem, or SelectedItem cell is being added, an event handler, PerformDataBinding, is added to the DataBinding event of the TableCell cell (line 14). After the DataGridItem that contains the TableCell cell has its DataItem property set, it will fire the DataBinding event, which will cause the PerformDataBinding event handler to execute. The PerformDataBinding event handler (lines 18 26), when executed, receives as its first input parameter the TableCell whose DataBinding event was wired up to the event handler. This input parameter, sender, is set to a local variable cell (line 19). Next, a DataGridItem local variable is created and used to reference the DataGridItem object that contains the TableCell whose DataBinding event has fired (line 20). Following that, the DataGridItem's DataItem property is referenced by the local variable dataItem (line 21). Finally, the Text property of the TableCell is set to the value of the DataField field in the dataItem object, assuming that the DataField property has been specified (lines 23 25) . NOTE When the added cell is an EditItem, Header, Footer, Pager, or SelectedItem, the event handler is not called. Instead, the cell's rendering is handled entirely by the DataGridColumn class's InitializeCell() method via the MyBase.InitializeCell method call on line 10. Censoring Offensive Words in the CensorColumn ClassAt this point, our CensorColumn mimics the functionality of the BoundColumn. To have the contents of the specified DataSource field censored, we need to provide a means for the contents of the cell's Text property to get rid of profanity. To accomplish this, we can use regular expressions to search for offensive words, replacing them with nonoffensive equivalents. NOTE String searching and replacing functions, such as String.IndexOf and String.Substring, can be used to perform censorship, but would require exponentially more time and code to include. Given a string, we can replace all instances of a substring with another substring using the following code: Dim strToCensor as String = "Hello, do you like my butt?" Dim strDirtyWord as String = "butt" Dim strCensoredWord as String = "behind" 'Replace all instances of "butt" with "behind" Dim re as New Regex("\b" & strDirtyWord & "\b", RegexOptions.IgnoreCase) Dim strCensoredString as String = re.Replace(strToCensor, strCensoredWord) This code uses the Regex.Replace method to search the string strToCensor, replacing all instances of strDirtyWord with strCensoredWord, returning the updated string. TIP Note that when creating the regular expression, the special character \b is placed before and after the value of strDirtyWord. In regular expressions, \b is a special character representing word boundaries. If we had omitted the \bs before and after strDirtyWord, the regular expression would replace all instances of "butt" with "behind", regardless of whether or not "butt" was part of a larger string. That is, had the \bs been omitted, the string "I like butter" would be censored to "I like behinder." To censor the contents of the TableCell, we can simply iterate through each possibly offensive word and use Regex.Replace to replace it with a less offensive one. But what words should be considered offensive, and what should their replacements be? It would be ideal to let the developer using the CensorColumn class to determine this. One option would be to add a Hashtable property to the CensorColumn class, which would enable the user to specify those words that should be censored, and what their replacements should be. Another approach would be to let the developer create an XML file whose contents specified what words should be censored. Let's implement the latter approach, although you're encouraged to experiment with the former approach as well. For our example, the XML file used must have the following structure: a root tag called censors, followed by zero to many censor tags, each of which contain a find and replace tag whose text contents are the words to censor and the words to replace the censored words, respectively. For example, if you want to censor the words butt, ugly, and stupid with behind, unattractive, and unintelligent, your XML file would look like this: <censors> <censor> <find>butt</find> <replace>behind</replace> </censor> <censor> <find>ugly</find> <replace>unattractive</replace> </censor> <censor> <find>stupid</find> <replace>unintelligent</replace> </censor> </censors> To use an XML file for censored words, the CensorColumn class will need another string property called CensorFile, which specifies the filename of the XML file to use. Listing 12.7 contains the code for the PerformCensorship function, which takes as input a string to censor and censors it based on the contents of the XML file specified by the CensorFile property. Listing 12.7 The PerformCensorship Function Censors the Words Specified in an XML File[View full width] 1: Private Function PerformCensorship(ByVal censorMe As String) As String 2: 'If no CensorFile has been specified, return censorMe string 3: If Me.CensorFile = String.Empty Then 4: Return censorMe 5: End If 6: 7: 'Convert the CensorFile path into a physical path 8: Dim filepath As String = HttpContext.Current.Server.MapPath(Me. CensorFile) 9: 10: 'Make sure the file exists 11: If Not File.Exists(filepath) Then 12: Throw New Exception("File " & filepath & " does not exist!") 13: Return censorMe 14: End If 15: 16: 'Read the XML file's contents 17: Dim censorDoc As New XmlDocument() 18: censorDoc.PreserveWhitespace = True 19: censorDoc.Load(filepath) 20: 21: Dim root As XmlNode = censorDoc.FirstChild 22: 23: Dim findNodes As XmlNodeList = root.SelectNodes("/censors/censor/find") 24: Dim replaceNodes As XmlNodeList = root.SelectNodes ("/censors/censor/replace") 25: 26: Dim re As Regex 27: Dim results As String = censorMe 28: Dim i As Integer 29: For i = 0 To findNodes.Count - 1 30: re = New Regex("\b" & findNodes.Item(i).InnerText & "\b", RegexOptions. The PerformCensorship function takes as input a string, censorMe, and returns a censored version of this input string. Before applying the censorship, though, a few quick checks are made. On line 3, a check is made to ensure that the CensorFile property has been specified. If it hasn't, the censorMe string is returned as is (line 4). Next, a check is made to see whether the file specified by the CensorFile property actually exists. First, on line 8, the CensorFile property is mapped to its physical file path. Next, on line 11, the File.Exists method is used to check whether the file exists. If the file does not exist, an Exception is thrown. NOTE The physical path of a file is given as DRIVELETTER:\DIRECTORY\...\FILENAME, like C:\Inetpub\wwwroot\profane.xml. To ease using the CensorColumn class in a DataGrid, the developer need only provide a CensorFile value that contains just the name of the XML file, like CensorFile="profane.xml". Assuming that the file profane.xml exists in the same directory as the ASP.NET Web page using the file, the CensorColumn class's PerformCensorship function will automatically compute the physical path of the file using the Server.MapPath method (line 11). If the control reaches line 16, then we know that a CensorFile property has been provided, and that it maps to the physical path specified by filepath. Therefore, we can load the contents of the XML file into an XmlDocument object (lines 17 19). Next, on line 21, the root node of the XmlDocument is retrieved. Then, two XmlNodeList instances are created findNodes and replaceNodes (lines 23 and 24). The findNodes XmlNodeList contains a list of all XML nodes whose path expression matches /censors/censor/find, whereas the replaceNodes XmlNodeList contains a list of all XML nodes whose path expression matches /censors/censor/replace. Additionally, a regular expression variable is created (line 26), and a string variable results is created and assigned the value of the censorMe input string parameter (line 27) . The nodes in the findNodes list are then enumerated (lines 29 32). For each XmlNode in the findNodes list, a new regular expression instance is created whose pattern is the InnerText of the particular findNode XmlNode (line 30). The contents of the local string variable results are then censored using the Replace method on line 31. Specifically, the contents of results are searched, and all instances of the InnerText of the current findNode are replaced with the InnerText of the current replaceNode. After this loop completes, the PerformCensorship function returns the value of results. CAUTION The code in the PerformCensorship function assumes that the XML file specified is in the proper format as described earlier. If the XML file has an invalid format for example, having a censor tag that is missing a replace tag an error will likely occur when iterating through the loop spanning lines 29 to 32. Use of the PerformCensorship function requires some changes to the CensorColumn code that was last presented in Listing 12.6. Specifically, a CensorFile property must be added, and the PerformDataBinding event handler must be updated to call the PerformCensorship function. Also, additional Imports statements need to be added for the XML, IO, and regular expression functionality used in the PerformCensorship function. These changes are reflected in Listing 12.8, which contains the complete CensorColumn code to this point. Listing 12.8 The Complete Code for the CensorColumn Class[View full width] 1: Imports System 2: Imports System.Text.RegularExpressions 3: Imports System.Web.UI.WebControls 4: Imports System.Web.UI 5: Imports System.Web 6: Imports System.Xml 7: Imports System.IO 8: 9: Public Class CensorColumn 10: Inherits DataGridColumn 11: 12: Public Overrides Sub InitializeCell(ByVal cell As TableCell, ByVal columnIndex As The new CensorFile property can be found on lines 73 through 80. The added Imports statements can be found on lines 2, 6, and 7. Finally, note that the PerformDataBinding event handler has been updated. On line 27, the cell.Text property is set to the string returned by the PerformCensorship function with the value of the DataSource field passed in as input. At this point, we can use the CensorColumn class as a DataGrid column in an ASP.NET Web page. Of course, before we can do so, we must compile the class into an assembly. As was done with the LimitColumn class examined in Listing 12.3, if you are using Visual Studio .NET, simply opt to add a new class to the SimpleColumn project. After you do this, rebuild the project and redeploy the SimpleColumn.dll to your Web application's /bin directory. If you are not using Visual Studio .NET, recompile the assembly using the command-line compiler, as was shown earlier. TIP If you are using the command-line compiler, be certain to add the Namespace SimpleColumn ... End Namespace statements to lines 8 and 91 in Listing 12.8, as discussed earlier. Listing 12.9 The CensorColumn Keeps the Online Guestbook Discussion Free from Profanity1: <%@ Register TagPrefix="custCols" Namespace="SimpleColumn" Assembly="SimpleColumn" %> 2: <%@ import Namespace="System.Data" %> 3: <%@ import Namespace="System.Data.SqlClient" %> 4: <script runat="server" language="VB"> 5: '... The Page_Load event handler and BindData() subroutine have 6: '... been omitted for brevity. They are strikingly similar to the 7: '... Page_Load event handler and BindData() subroutine from Listing 12.2, 8: '... with the exception that BindData() retrieves the records from the 9: '... Comments table instead of the title table ... 10: </script> 11: 12: <asp:DataGrid runat="server" 13: AutoGenerateColumns="False" 14: Font-Name="Verdana" Font-Size="9pt" 15: HeaderStyle-HorizontalAlign="Center" 16: HeaderStyle-Font-Bold="True" 17: HeaderStyle-BackColor="Navy" HeaderStyle-ForeColor="White" 18: AlternatingItemStyle-BackColor="#eeeeee"> 19: <Columns> 20: <asp:BoundColumn HeaderText="Name" DataField="Name" /> 21: <custCols:CensorColumn HeaderText="Comment" DataField="Comment" 22: CensorFile="profane.xml" /> 23: <asp:BoundColumn DataField="DateAdded" HeaderText="Date" /> 24: </Columns> 25: </asp:DataGrid> The ASP.NET code in Listing 12.9 contains a DataGrid that displays the rows of the Comments table. A CensorColumn class is used to display the actual comment made by the user (lines 21 and 22). An XML file, profane.xml, located in the same folder as the ASP.NET Web page, contains the following content: <censors> <censor> <find>jerk</find> <replace>j*rk</replace> </censor> <censor> <find>stupid</find> <replace>st***d</replace> </censor> <censor> <find>ignoramus</find> <replace>uneducated individual</replace> </censor> </censors> As you can see, it censors the words jerk, stupid, and ignoramus, replacing them with j*rk, st***d, and uneducated individual. Figure 12.4 shows a screenshot of the DataGrid using the CensorColumn class. Note that the word jerk has been replaced by j*rk in John's first comment. Also, stupid is replaced by st***d in Frank's comment, and both John and Frank have instances of ignoramus replaced by uneducated individual. Figure 12.4. Offensive comments have been censored in the DataGrid's Comments column through the use of the custom CensorColumn class. Providing a Default Editing Interface for a Custom DataGrid Column ClassIn the InitializeCell() method in Listing 12.8, we check to see whether the cell being added is of type Item, AlternatingItem, or SelectedItem (see line 16, Listing 12.8). If it is one of these types, we assign the TableCell's DataBinding event handler to the PerformDataBinding event handler. What if, though, the cell being rendered is of type EditItem? NOTE Recall from Chapter 9 that when a DataGrid row is selected for editing, the row is said to be in "edit mode," meaning that the DataGridItem representing the row in edit mode has its ItemType property set to ListItemType.EditItem. One option would be to not provide an editing interface, instead just displaying the text of the DataSource field specified by the DataField property and censoring the contents. To accomplish this, we would need to simply adjust line 16 of Listing 12.8 to include ListItemType.EditItem in the Case statement:
However, it would be more useful to provide our CensorColumn with the same editing functionality as the BoundColumn control. That is, when a row enters "edit mode," the CensorColumns should display the content in a standard TextBox Web control. In this editing interface, however, we don't want to display the text of the message as censored, because the administrator might want to remove the offensive words from the post. (In addition, keep in mind that the uncensored version of the message is what is actually stored in the database. ) To provide a default editing interface for our CensorColumn, we'll need to check to see whether the itemType property is equal to ListItemType.EditItem. If it is, we want to add a TextBox Web control to the TableCell in the InitializeCell() method. We can then create a new event handler for the TextBox's DataBinding event or simply reuse the TableCell's DataBinding event handler. The code in Listing 12.10 uses the latter approach. The challenge involved with having the same DataBinding event handler being used for both edit mode and non-edit mode is that in the event handler, we must be able to determine whether the row being added is in edit mode. If it is in edit mode, we want to assign the DataSource field value to the TextBox's Text property; if it is not in edit mode, we want to set the TableCell's Text property to the censored version of the DataSource field, as we did in Listing 12.8. There are a number of ways to accomplish this one way is to add an IsBeingEdited private member variable to the CensorColumn class. This variable is set to True when a cell with itemType EditItem is being rendered, and is set to False otherwise. Then, in the PerformDataBinding event handler, the value of IsBeingEdited is checked to determine whether to set the TableCell's Text property to the censored results of the DataSource field specified by DataField, or if the TextBox's Text property should be set to the results of the DataSource field instead. Listing 12.10 contains updated code for the CensorColumn's InitializeCell() method and PerformDataBinding event handler. With the following changes, the CensorColumn provides a default editing interface of a TextBox. Listing 12.10 The Updated InitializeCell Method and PerformDataBinding Event Handler Provides a Default Editing Interface for the CensorColumn Control[View full width] 1: Private IsBeingEdited As Boolean = False 2: 3: Public Overrides Sub InitializeCell(ByVal cell As TableCell, ByVal columnIndex As The code in Listing 12.10 belongs in the CensorColumn class provided in Listing 12.8. Note that with these changes, we've added a private member variable, IsBeingEdited, which is of type Boolean (line 1). In the InitializeCell() method, an additional Case statement was added to the Select Case starting on line 6. Specifically, the new Case statement checks to see whether the itemType property is set to ListItemType.EditItem (line 7) if it is, a TextBox is added to the TableCell cell's Controls collection (lines 9 11). In addition, the IsBeingEdited member variable is set to True. Finally, the TableCell cell's DataBinding event is wired up to the PerformDataBinding event handler (line 14). If the cell being rendered is of type Item, AlternatingItem, or SelectedItem, the IsBeingEdited member variable is set to False (line 17) before the TableCell cell's DataBinding event is wired up to the PerformDataBinding event handler (line 18). On line 29 in the PerformDataBinding event handler, the IsBeingEdited property is checked. If it is False, then cell's Text property is assigned the censored value of the appropriate DataSource field, just as it was in Listing 12.8. However, if the IsBeingEdited property is True, the cell's TextBox is referenced (line 32) and its Text property is set to the value of the appropriate DataSource field (line 33). Listing 12.11 illustrates the CensorColumn in use in an ASP.NET Web page with an editable DataGrid. Note that when a particular row is to be edited, the CensorColumn is rendered as a standard TextBox control. Listing 12.11 The CensorColumn's Default Editing Interface Is a TextBox Web Control1: <%@ Register TagPrefix="custCols" Namespace="SimpleColumn" Assembly="SimpleColumn" %> 2: <%@ import Namespace="System.Data" %> 3: <%@ import Namespace="System.Data.SqlClient" %> 4: <script runat="server" language="VB"> 5: '... The Page_Load event handler and BindData() subroutine have 6: '... been omitted for brevity. They are strikingly similar to the 7: '... Page_Load event handler and BindData() subroutine from Listing 12.2, 8: '... with the exception that BindData() retrieves the records from the 9: '... Comments table instead of the title table ... 10: 11: Sub dgComments_EditRow(sender as Object, e as DataGridCommandEventArgs) 12: dgComments.EditItemIndex = e.Item.ItemIndex 13: End Sub 14: </script> 15: 16: <form runat="server"> 17: <asp:DataGrid runat="server" 18: AutoGenerateColumns="False" 19: Font-Name="Verdana" Font-Size="9pt" 20: HeaderStyle-HorizontalAlign="Center" 21: HeaderStyle-Font-Bold="True" 22: HeaderStyle-BackColor="Navy" HeaderStyle-ForeColor="White" 23: AlternatingItemStyle-BackColor="#eeeeee" 24: 25: OnEditCommand="dgComments_EditRow"> 26: <Columns> 27: <asp:EditCommandColumn EditText="Edit" UpdateText="Update" 28: CancelText="Cancel" /> 29: <asp:BoundColumn HeaderText="Name" DataField="Name" /> 30: <custCols:CensorColumn HeaderText="Comment" DataField="Comment" 31: CensorFile="profane.xml" /> 32: <asp:BoundColumn DataField="DateAdded" HeaderText="Date" /> 33: </Columns> 34: </asp:DataGrid> 35: </form> The code in Listing 12.11 should look familiar, as it's quite similar to code we examined in Chapter 9. Specifically, the DataGrid is placed inside a server-side form (see lines 16 and 35) and is configured to be edited. This includes adding the OnEditCommand on line 25 and providing an EditCommandColumn (lines 27 and 28). The dgComments_EditRow event handler (lines 11 13) simply sets the EditItemIndex property of the dgComments DataGrid to the index of the row whose Edit button was clicked. NOTE A full, working example of an editable DataGrid would, of course, also include event handlers for when the user clicks the Update and Cancel buttons. These event handlers have been omitted in Listing 12.11 for brevity. Figure 12.5 contains a screenshot of Listing 12.11 when viewed through a browser. Note that the row being edited has its CensorColumn rendered as a standard TextBox, and that the contents of the TextBox contain censoring. Figure 12.5. The CensorColumn's default editing interface is a TextBox. Adding a ReadOnly Property to the CensorColumn ClassRecall that the BoundColumn control contains a ReadOnly property that, when set to True, marks the column as read-only, meaning that for a row in edit mode, the column will be rendered as a textual label instead of a TextBox. This would be a nice feature to add to the CensorColumn control, and can be added with only a few lines of code. Specifically, all we need to do is add a ReadOnly property to our class, and then in the InitializeCell() method, check the value of this property before creating and adding a TextBox to the TableCell cell. The code in Listing 12.12 contains the complete code for the CensorColumn class, including the addition of the ReadOnly property. The pieces added to handle the ReadOnly feature are located on lines 94 through 103, where the ReadOnly property is added, and on lines 19 to 27, where the ReadOnly property is checked to determine whether a TextBox should be added to the TableCell cell. In the InitializeCell() method, if the cell being rendered is for a row in edit mode, the itemType will be set to ListItemType.EditItem, meaning that the Case statement starting on line 18 will execute. If the ReadOnly property is True, the IsBeingEdited member variable is set to False (line 20). It is vital that we set the IsBeingEdited member variable to False here, because it will ensure that the TableCell's Text property is set to the censored value of the specified DataSource field in the PerformCensorship event handler. In the case that ReadOnly is False, the code from line 22 to line 26 is executed. This code, which we examined in Listing 12.10, adds a TextBox to the TableCell cell, and sets the IsBeingEdited member variable to True, causing the TextBox's Text property to be assigned the uncensored version of the specified DataSource field in the PerformCensorship event handler. CAUTION Notice on line 96 that the ReadOnly property has brackets around the word ReadOnly. This is because ReadOnly is a keyword in Visual Basic .NET. If you forget to place these brackets around ReadOnly, a compile-time error will result, so be certain to include them. (Another option would be to just use a property name that isn't a reserved keyword in the language, such as IsReadOnly or CannotWrite.) Listing 12.12 The Complete Code for the CensorColumn Class[View full width] 1: Imports System 2: Imports System.Text.RegularExpressions 3: Imports System.Web.UI.WebControls 4: Imports System.Web.UI 5: Imports System.Web 6: Imports System.Xml 7: Imports System.IO 8: 9: Public Class CensorColumn 10: Inherits DataGridColumn 11: 12: Private IsBeingEdited As Boolean = False 13: 14: Public Overrides Sub InitializeCell(ByVal cell As TableCell, ByVal columnIndex |