Steps for Creating a Robust Custom DataGrid Column

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.

graphics/12fig03.gif

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 Class

The 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  graphics/ccc.gifInteger, ByVal itemType As ListItemType)  10:         MyBase.InitializeCell(cell, columnIndex, itemType)  11:  12:         ' TODO: Bind the  13:     End Sub  14:  15:     Private m_dataField As String  16:  17:     Public Property DataField() As String  18:         Get  19:             Return Me.m_dataField  20:         End Get  21:         Set(ByVal Value As String)  22:             Me.m_dataField = Value  23:         End Set  24:     End Property  25: End Class 

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  graphics/ccc.gifInteger, ByVal itemType As ListItemType)  10:         MyBase.InitializeCell(cell, columnIndex, itemType)  11:  12:         Select Case itemType  13:             Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType. graphics/ccc.gifSelectedItem  14:                 AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding  15:         End Select  16:     End Sub  17:  18:     Private Sub PerformDataBinding(ByVal sender As Object, ByVal e As EventArgs)  19:         Dim cell As TableCell = sender  20:         Dim gridItem As DataGridItem = cell.NamingContainer  21:         Dim dataItem As Object = gridItem.DataItem  22:  23:         If Me.DataField <> String.Empty Then  24:             cell.Text = DataBinder.Eval(dataItem, Me.DataField)  25:         End If  26:     End Sub  27:  28:  29:     Private m_dataField As String  30:   31:     Public Property DataField() As String  32:         Get  33:             Return Me.m_dataField  34:         End Get  35:         Set(ByVal Value As String)  36:             Me.m_dataField = Value  37:         End Set  38:     End Property  39: End Class 

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 Class

At 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. graphics/ccc.gifIgnoreCase)  31:           results = re.Replace(results, replaceNodes.Item(i).InnerText)  32:       Next  33:  34:       Return results  35:   End Function 

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  graphics/ccc.gifInteger, ByVal itemType As ListItemType)  13:         MyBase.InitializeCell(cell, columnIndex, itemType)  14:  15:         Select Case itemType  16:             Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType. graphics/ccc.gifSelectedItem  17:                 AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding  18:         End Select  19:     End Sub  20:  21:     Private Sub PerformDataBinding(ByVal sender As Object, ByVal e As EventArgs)  22:         Dim cell As TableCell = sender  23:         Dim gridItem As DataGridItem = cell.NamingContainer  24:         Dim dataItem As Object = gridItem.DataItem  25:  26:         If Me.DataField <> String.Empty Then  27:             cell.Text = PerformCensorship(DataBinder.Eval (dataItem, Me.DataField))  28:         End If  29:     End Sub  30:  31:  32:     Private Function PerformCensorship(ByVal censorMe As String) As String  33:         'If no CensorFile has been specified, return censorMe string  34:         If Me.CensorFile = String.Empty Then  35:             Return censorMe  36:         End If  37:  38:         'Convert the CensorFile path into a physical path  39:         Dim filepath As String = HttpContext.Current.Server. MapPath(Me.CensorFile)  40:  41:         'Make sure the file exists  42:         If Not File.Exists(filepath) Then  43:             Throw New Exception("File " & filepath & " does not exist!")  44:             Return censorMe   45:         End If  46:  47:         'Read the XML file's contents  48:         Dim censorDoc As New XmlDocument()  49:         censorDoc.PreserveWhitespace = True  50:         censorDoc.Load(filepath)  51:  52:         Dim root As XmlNode = censorDoc.FirstChild  53:  54:         Dim findNodes As XmlNodeList = root.SelectNodes ("/censors/censor/find")  55:         Dim replaceNodes As XmlNodeList = root.SelectNodes ("/censors/censor/ graphics/ccc.gifreplace")  56:  57:         Dim re As Regex  58:         Dim results As String = censorMe  59:         Dim i As Integer  60:         For i = 0 To findNodes.Count - 1  61:             re = New Regex("\b" & findNodes.Item(i).InnerText & "\b", RegexOptions. graphics/ccc.gifIgnoreCase)  62:             results = re.Replace(results, replaceNodes.Item(i).InnerText)  63:         Next  64:  65:         Return results  66:     End Function  67:  68:  69:  70:     Private m_dataField As String  71:     Private m_censorFile As String  72:  73:     Public Property CensorFile() As String  74:         Get  75:             Return Me.m_censorFile  76:         End Get  77:         Set(ByVal Value As String)  78:             Me.m_censorFile = Value  79:         End Set  80:     End Property  81:  82:     Public Property DataField() As String   83:         Get  84:             Return Me.m_dataField  85:         End Get  86:         Set(ByVal Value As String)  87:             Me.m_dataField = Value  88:         End Set  89:     End Property  90: End Class 

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 Profanity
  1: <%@ 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.

graphics/12fig04.gif

Providing a Default Editing Interface for a Custom DataGrid Column Class

In 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:

[View full width]

Select Case itemType Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType. SelectedItem, graphics/ccc.gifListItemType.EditItem AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding End Select

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  graphics/ccc.gifInteger, ByVal itemType As ListItemType)   4:     MyBase.InitializeCell(cell, columnIndex, itemType)   5:   6:     Select Case itemType   7:         Case ListItemType.EditItem   8:             'Add a TextBox   9:             Dim txtProfane As New TextBox()  10:             txtProfane.ID = "txtProfane"  11:             cell.Controls.Add(txtProfane)  12:             IsBeingEdited = True   13:  14:             AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding  15:  16:         Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType.  graphics/ccc.gifSelectedItem  17:             IsBeingEdited = False  18:             AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding  19:     End Select  20: End Sub  21:  22:  23: Private Sub PerformDataBinding(ByVal sender As Object, ByVal e As EventArgs)  24:     Dim cell As TableCell = sender  25:     Dim gridItem As DataGridItem = cell.NamingContainer  26:     Dim dataItem As Object = gridItem.DataItem  27:  28:     If Me.DataField <> String.Empty Then  29:         If Not IsBeingEdited Then  30:             cell.Text = PerformCensorship(DataBinder.Eval(dataItem, Me.DataField))  31:         Else  32:             Dim txtProfane As TextBox = cell.FindControl("txtProfane")  33:             txtProfane.Text = DataBinder.Eval(dataItem, Me.DataField)  34:         End If  35:     End If  36: End Sub 

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 Control
  1: <%@ 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.

graphics/12fig05.gif

Adding a ReadOnly Property to the CensorColumn Class

Recall 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  graphics/ccc.gifAs Integer, ByVal itemType As ListItemType)   15:         MyBase.InitializeCell(cell, columnIndex, itemType)   16:   17:         Select Case itemType   18:             Case ListItemType.EditItem   19:                 If Me.ReadOnly Then   20:                     IsBeingEdited = False   21:                 Else   22:                     'Add a TextBox   23:                     Dim txtProfane As New TextBox()   24:                     txtProfane.ID = "txtProfane"   25:                     cell.Controls.Add(txtProfane)   26:                     IsBeingEdited = True   27:                 End If   28:   29:                 AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding   30:   31:             Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType. graphics/ccc.gifSelectedItem   32:                 IsBeingEdited = False   33:                 AddHandler cell.DataBinding, AddressOf Me.PerformDataBinding   34:         End Select   35:     End Sub   36:   37:   38:     Private Sub PerformDataBinding(ByVal sender As Object, ByVal e As EventArgs)   39:         Dim cell As TableCell = sender   40:         Dim gridItem As DataGridItem = cell.NamingContainer   41:         Dim dataItem As Object = gridItem.DataItem   42:   43:         If Me.DataField <> String.Empty Then   44:             If Not IsBeingEdited Then   45:                 cell.Text = PerformCensorship(DataBinder. Eval(dataItem, Me. graphics/ccc.gifDataField))   46:             Else   47:                 Dim txtProfane As TextBox = cell.FindControl("txtProfane")   48:                 txtProfane.Text = DataBinder.Eval(dataItem, Me.DataField)    49:             End If   50:         End If   51:     End Sub   52:   53:   54:     Private Function PerformCensorship(ByVal censorMe As String) As String   55:         'If no CensorFile has been specified, return censorMe string   56:         If Me.CensorFile = String.Empty Then   57:             Return censorMe   58:         End If   59:   60:         'Convert the CensorFile path into a physical path   61:         Dim filepath As String = HttpContext.Current.Server. MapPath(Me.CensorFile)   62:   63:         'Make sure the file exists   64:         If Not File.Exists(filepath) Then   65:             Throw New Exception("File " & filepath & " does not exist!" )   66:             Return censorMe   67:         End If   68:   69:         'Read the XML file's contents   70:         Dim censorDoc As New XmlDocument()   71:         censorDoc.PreserveWhitespace = True   72:         censorDoc.Load(filepath)   73:   74:         Dim root As XmlNode = censorDoc.FirstChild   75:   76:         Dim findNodes As XmlNodeList = root.SelectNodes("/censors/ censor/find")   77:         Dim replaceNodes As XmlNodeList = root.SelectNodes("/censors/ censor/ graphics/ccc.gifreplace")   78:   79:         Dim re As Regex   80:         Dim results As String = censorMe   81:         Dim i As Integer   82:         For i = 0 To findNodes.Count - 1   83:             re = New Regex("\b" & findNodes.Item(i).InnerText & "\b", RegexOptions. graphics/ccc.gifIgnoreCase)   84:             results = re.Replace(results, replaceNodes.Item(i).InnerText)    85:         Next   86:   87:         Return results   88:     End Function   89:   90:   91:   92:     Private m_dataField As String   93:     Private m_censorFile As String   94:     Private m_readOnly As Boolean   95:   96:     Public Property [ReadOnly]() As String   97:         Get   98:             Return Me.m_censorFile   99:         End Get  100:         Set(ByVal Value As String)  101:             Me.m_censorFile = Value  102:         End Set  103:     End Property  104:  105:  106:     Public Property CensorFile() As String  107:         Get  108:             Return Me.m_censorFile  109:         End Get  110:         Set(ByVal Value As String)  111:             Me.m_censorFile = Value  112:         End Set  113:     End Property  114:  115:     Public Property DataField() As String  116:         Get  117:             Return Me.m_dataField  118:         End Get  119:         Set(ByVal Value As String)  120:             Me.m_dataField = Value  121:         End Set  122:     End Property  123: End Class 


ASP. NET Data Web Controls Kick Start
ASP.NET Data Web Controls Kick Start
ISBN: 0672325012
EAN: 2147483647
Year: 2002
Pages: 111

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net