Download the code for this article: Cutting0201.exe (713KB) | |
In contrast, templates involve deeper changes that modify some portions of a control's user interface. For example, the Repeater control allows you to use a combination of HTML elements and ASP.NET controls to define the layout for each row in the bound data source. Likewise, the DataGrid control lets you format the cells of an entire column of data by using any valid combination of ASP.NET controls. In addition, the DataGrid control supports customization of rows by applying different templates to individual rows, alternating rows, selected rows, and so on. The overall pattern behind ASP.NET templates is not very different from that in the visual styles of Windows® XP or even Windows common controls. The key point to understand is that certain parts of a given user interface element can be customized. You have client and non-client parts in Win32® common controls, control-specific parts in Windows XP styles, and control-specific templates in certain ASP.NET server controls. In Win32, messages are used to let programmers get involved with the painting process. In Windows XP, the theme manager calls the piece of code registered to paint part of a given control. In ASP.NET, control classes that inherit the ITemplate interface can dynamically build pagelets and insert code into the main page as raw HTML. Styles and templates are not mutually exclusive. You can use styles and templates together, or use them separately to control the appearance of the elements defined within your templates. When the templated control (say a DataGrid) is processed, templates are instantiated in the containing page and rendered in HTML. In this column, I'll cover ASP.NET templates with respect to the features provided by the three iterative controls: Repeater, DataList, and DataGrid. They all support templates to increase their own level of flexibility and to help you customize the graphical presentation of your data. In addition, I'll illustrate a few techniques for loading templates programmatically and discuss how they could be applied to create a multi-selection DataGrid control. In all the examples, I'll be using the Employees table of the SQL Server™ 2000 Northwind database. What is a Template, Anyway? Generally speaking, a template is the description of how a certain element will be rendered at runtime. In ASP.NET, a template is property of a server control that describes the static HTML, controls, and script to render within one region of the control. For example, a Repeater control has a HeaderTemplate property that defines the contents of its header region. You normally define a template within the body of an ASP.NET page using declarative syntax. For example, the following code shows how to specify a template to draw the header and each row of a Repeater control. <asp:repeater runat="server"> <HeaderTemplate> <h2>Employees</h2> <table border="0"> </HeaderTemplate> <ItemTemplate> <tr>...</tr> </ItemTemplate> </asp:repeater>When it comes to rendering the contents of the Repeater control, the ASP.NET runtime uses the content defined in the templates and processes it—often together with bound data—to create an HTML representation of the region. All the server-side controls within the template individually render themselves as HTML. The Microsoft® .NET Framework utilizes the ITemplate interface at runtime to process the templates into a control hierarchy that can be databound and rendered to populate a part of an ASP.NET control. As long as you define templates in a declarative manner—using inline tags in ASPX pages—you don't strictly need to know about the ITemplate interface. That will become important only when you move one step further and start investigating how to create and assign templates programmatically and dynamically. But before we look at that, let's review the use of templates with a DataGrid control. DataGrid's Templated Columns Template-based columns in a Web DataGrid control play an important role as they allow you to add a freeform column type to the DataGrid. Normally, the DataGrid control displays all of its contents through plain text strings (as BoundColumns), or through one of the pre-defined column types. However, sometimes the pre-defined column types just don't provide the representation the page developer is after. A templated column can define up to four different templates, as explained in Figure 1. You will likely use the ItemTemplate most frequently. It defines how the nth cell of the column will draw and what its contents will be in terms of constituent controls. HeaderTemplate and FooterTemplate are rather self-explanatory. The EditItemTemplate property lets you specify how the cell will change when the parent row is put into edit mode. Note that, unlike the DataList control, the DataGrid does not feature a template for the selected state. Your DataGrid control should use template-based columns when you need to do something in a nonstandard way throughout the whole column. If you need to display data in a way that none of the base column types provide for (text, buttons, hyperlinks), then templated columns are your best choice. The following code shows how to bind a templated column to a DataGrid control. <asp:TemplateColumn runat="server" HeaderText="heading"> <itemtemplate> ...ASP.NET layout goes here... </itemtemplate> </asp:TemplateColumn>Notice that a templated column, like any other column type, can have a header text as well as a sorting expression. Templated columns, though, do not have an explicit data source field to bind to. Among the members of the TemplateColumn class, you will not find any DataField or DataTextField property. The lack of an explicit data-bound property is justified by the extreme flexibility that is the ultimate goal of such a column layout. To render a column, you could use, say, a Label control that does have the Text property, but also a dropdown list control or an image, both of which don't have anything like the Text property. As a result, you must always use data binding expressions to bind to data. While they're often too verbose in my opinion, they also give you unprecedented flexibility. The following code snippet represents valid content for an item template: <asp:label runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "lastname") %>' />By using DataBinder.Eval, you can access any field in the currently bound data source. In addition, you can combine them in any order to obtain any sort expression that's otherwise impossible using a simpler bound or button column. Figure 2 Column In Figure 2, you can see a sample template-based column in which simple HTML formatting has been applied to a couple of database fields. This result cannot be obtained with per-field, non-template-based binding. The item template code looks like the following: <itemtemplate> <%# "<b>" + DataBinder.Eval(Container.DataItem, "lastname") + "</b>, " + DataBinder.Eval(Container.DataItem, "firstname") %> </itemtemplate>If you need to combine more fields in the same displayable format, templated columns are the only way to go. If you need to apply any special formatting or feature to the cell, you are better off hooking to the ItemCreated or the ItemDataBound grid events. For example, if you need to change the background color of a cell, or any other style property based on a condition, you write a handler for ItemCreated, make sure the item being created is exactly of the type you need (typically Item or AlternatingItem), and then merge new and existing styles. When the ItemCreated event fires, though, there is no guarantee that the item has been bound to data already. The data is normally available through the DataItem property of the event data structure. For the DataGrid's ItemCreated event, this structure is DataGridItemEventArgs. void ItemCreated(Object sender, DataGridItemEventArgs e)The expression e.Item.DataItem evaluates to the data associated with the item being created. If the grid binds to a DataTable, then DataItem is a DataRow object. As I mentioned earlier, though, in most cases the data binding has not yet occurred at the time the ItemCreated event fires. In fact, data binding normally takes place when the ItemDataBound event is raised. This rule has just one exception regarding template-based columns. A templated column is not explicitly bound to a single field in the data source; it can access the whole data item. This can be done during ItemCreated without waiting for ItemDataBound, which is an event that fires later than ItemCreated. For bound, button, and hyperlink columns, you will need to hook the ItemDataBound event to preview the data-bound text that the grid is going to display. Templated Headers Since the TemplateColumn class allows for a HeaderTemplate property, you can also customize the header (and the footer) of a given column. Speaking of advanced customization, there is an important point to be made here. You can never have plain data-bound columns with a templated header or footer. Templates apply only to instances of the TemplateColumn class. For instance, if you want to edit the contents of a column in a nonstandard way (let's say you want to add some validators), then you have to use templates throughout, even though the column renders well with the simpler BoundColumn class. Changing the layout of the header, though, can be problematic if you need to sort that column by a certain expression. The sorting mechanism is triggered by a hyperlink control that the DataGrid control automatically embeds in the column heading. The href property of this hyperlink control generates a postback event when the user clicks on the element. The target of the hyperlink is client-side JavaScript whose internals have not yet been fully documented with ASP.NET Beta 2. Feel free to change the layout of a column heading as long as you don't need sorting. If you do need it, use the ItemCreated event to add extra controls to the header and leave the DataGrid control free to generate everything that is needed for sorting. The code in Figure 3 dynamically adds a dropdown list to the header of a template column to let you choose the expression to sort by. Customizing the header is useful when you have templated columns that group together several fields. In this case, you might want to allow users to sort by a variety of fields, as shown in Figure 4. Figure 4 Sort by Field The code in Figure 3 creates a dynamic dropdown list with the available sort expressions. Next, it retrieves the currently selected expression when users click on the header's hyperlink. The header text of the template column must be set to a non-empty string so that all the standard infrastructure for sorting can be built and set up to work properly. <asp:TemplateColumn runat="server" HeaderText="Sort by" SortExpression="*">Notice the unusual value (an asterisk) assigned to the SortExpression property. It plays a key role since it allows the sort command handler (see Figure 3) to recognize that the user clicked on a column that requires special treatment for sorting. The SortExpression attribute, in fact, is the only element you have to recognize the clicked column. Figure 5 Grouping Columns In Figure 5 you can see another special type of column header—one that groups two or more physical columns. This solution turns out to be rather useful when you have templated columns of data where alignment is important. In particular, the sample utilizes this trick to align the title of courtesy with first and last name. The grid contains two distinct columns: one BoundColumn for the title of courtesy and one TemplateColumn for the employee name. Both columns have their own caption, but during the ItemCreated event one of the two header cells is removed while the other is expanded with ColumnSpan to cover the space of the side column. Which cell you remove is up to you. // 1 means second column in the grid cell = (TableCell) e.Item.Cells[1]; e.Item.Cells.Remove(cell); cell = (TableCell) e.Item.Cells[1]; cell.ColumnSpan = 2;Adapt UI to Data Template-based columns help considerably when you want to model the control's user interface to accommodate the actual data to be displayed. Not all data in a data grid would be easy to read and understand if rendered as plain text. This is particularly true for Booleans, arrays, and images. Depending on their real meaning and role in the context of the application, Boolean data can be better rendered as a pair of mutually exclusive Yes/No strings or with small pictures representing checked and unchecked controls. Using pictures instead of a true checkbox control is more effective because the image is not clickable and never takes the focus, which results in a more natural user interface (see Figure 6). Figure 6 Column with Checkboxes The ImageUrl property of the <asp:image> element is data-bound and can be associated with a binding expression. <itemtemplate> <asp:image runat="server" imageurl='<%# GetProperFile( (int) DataBinder.Eval(Container.DataItem, "paid")) %>' /> </itemtemplate>In this code, the binding expression consists of a user-defined function that takes the value of a Boolean field as an argument. The function returns the name of the proper GIF file to be used within the resulting HTML <img> tag. Sometimes, though, the ideal representation of Boolean data may not require two mutually exclusive images or text. For example, a column that will represent whether a given invoice has been paid can be effectively rendered through a checkmark if the field evaluates to true and to nothing if it hasn't. In this simple case, you could elegantly solve the issue by intercepting the cell creation through the ItemDataBound event. The code needed to do this is shown in Figure 7, while Figure 8 shows the results. Figure 8 Column with Checks In general, if you can work around the data representation issue and do without template-based columns, then by all means do so. Templates are powerful and effective, but using them is always a bit heavier than hooking events like ItemDataBound and ItemCreated. In terms of raw performance the difference between the two approaches is probably not very meaningful for most applications. In short, always choose the most flexible solution with a preference for event hooking. The code in Figure 7 addresses an interesting point that is even more evident if the information to be displayed in a given column consist of images. What could you do if, for one reason or another, the image for one row is not available? You might want to show some alternate text or simply leave the cell blank. To insert images in a column there are two possible approaches: you can either write a new column object or you can use a templated column based on an <asp:image> element. The former approach is preferable because all the details will be hidden from view and buried in the class code. A new type of column would also be the right place to implement special features like an alternate layout to be used when the necessary image is missing or just won't render. Likewise, an image column could be adapted to load the bits, and not the path, of the image directly from a blob database field. I'll reserve this particular topic for a future column. In the meantime, let's look at how to show images and text in the same column using templates. As you will see, the technique is generic and can be employed with any column where two or more layouts can be applied, depending on runtime conditions. The idea is to define two layouts within the same template column, but display only one at a time. The layout to display can be decided either in the ItemCreated event handler or through a simpler data binding expression. Figure 9 shows the ASP.NET code for the DataGrid control that employs such a template. The item template has an <img> and an <asp:label> element. Both explicitly assign their respective Visible property. The label control features fixed text. Normally, you should avoid using labels with static text. When the text is not expected to change, you are better off using client-side HTML elements like <span> that are not marked to work on the server (they have no runat=server attribute). In this case, though, the value for the Visible property must be evaluated on the server, so using <asp:label> is a necessity whether it features static text or not. The visibility of the controls representing mutually exclusive layouts for the column is controlled by a user-defined function called IsImageAvailable. bool IsImageAvailable(String strLastName) { String strImageFile = "images//"; strImageFile = strLastName + ".bmp"; return File.Exists( Server.MapPath(strImageFile)); }The function assumes that the images to display are BMP files located in the Images subfolder of the app path and are named for the last name of the employee. Of course, this is clearly arbitrary. In your own applications you might use smarter solutions. But you get the point. Figure 10 shows what the final page will look like when some images are missing from the collection. Figure 10 Missing Image Loading Templates Programmatically Normally, templated columns have their layout code defined at design time. While this remains the most common circumstance, there might be situations in which using design-time templates is just not the best solution. If you know in advance that a lot of changes must be applied at runtime through events like ItemCreated and ItemDataBound, there is no reason to define a static template forcing the control to support a double effort, processing the template first and the changes next. Another relatively frequent situation in which a dynamic template is preferable is when users must be able to change views of the same data. You can load templates dynamically in at least two ways. First, you can store the layout code of the template in a user control file, save it with an ASCX extension, and load it back through the LoadTemplate method of the Page object. The second alternative requires you to create a tailor-made class which implements the ITemplate interface. Let's look at both in more detail. To create a DataGrid column dynamically, you create a new instance of the specified class, populate its properties as appropriate, and then add the object to the DataGrid's Columns collection property. The following code demonstrates how to proceed. TemplateColumn bc = new TemplateColumn(); bc.HeaderText = "Template Column"; bc.ItemTemplate = Page.LoadTemplate(TEMPLATEFILE); grid.Columns.Add(bc);First, you create a new instance of the TemplateColumn class and set the header text and other properties as required for your application. The tricky part is how you specify a template for ItemTemplate and any other template property you may need. The simplest way is through an external ASCX file. ASCX files are user controls that declaratively define the HTML text and the ASP.NET controls that form the template. The following is the typical structure of a valid ASCX file. <%@ Language="C#" %> <%# layout goes here %>It is extremely important that you explicitly indicate the language used throughout the pagelet. You have to do this even though nothing in the ASCX code seems to be language-dependent. You can use any .NET-compliant language and even a language different from the one used within the host page. The Page.LoadTemplate method can be used to load the layout code for any template property of the column, including EditItemTemplate and HeaderTemplate. You don't need to use a fully qualified Web path when calling LoadTemplate because the method expects to receive a virtual path. You cannot specify an absolute path with directory information. Templates from Strings Unlike many other methods that accomplish similar tasks, the Page.LoadTemplate method does not support streams and writers. If this were possible at all, then in-memory strings could have been used to create dynamic templates. If you don't want to or can't afford to make your application dependent on external ASCX files, how can you create dynamic templates? Typically, you don't want to deal with ASCX files when the layout code is just one of the configuration parameters of the application all stored, say, in a SQL Server-based table or an XML file. Is there a way to dynamically create a template from a string? Looking at the programming interface of the involved classes, the answer is certainly no. However, nothing really prevents you from creating a temporary file and loading a template from there. When you create temporary files from within an ASP.NET application, make sure that the file name is really unique for each concurrent session. For this purpose, use the Session ID or create a unique temporary file through the static method Path.GetTempFileName. Bear in mind, that the template file must have an .ascx extension. Also, as mentioned earlier, the LoadTemplate method assumes that the file has a virtual path and returns an error if you force it to work on absolute paths. On the other hand, stream and writer classes require absolute paths and don't know how to cope with virtual paths. As a result, you need code like the following to create and load a string-based template: TemplateColumn bc = new TemplateColumn(); String tmp = Session.SessionID + ".ascx"; StreamWriter sw; sw = new StreamWriter(Server.MapPath(tmp)); sw.Write(strLayoutCode); sw.Close(); bc.ItemTemplate = Page.LoadTemplate(tmp); grid.Columns.Add(bc); File.Delete(Server.MapPath(tmp));You need to use Server.MapPath to map a URL from a virtual to a physical path, but only when working with streams and files. Implementing ITemplate If you want to create a template completely in memory, you have to first code and then instantiate a class which implements the ITemplate interface. The ITemplate interface has only one method, which is called InstantiateIn. Any template property in all ASP.NET server controls is exposed as a property of a class that implements the ITemplate interface. This interface simply defines the method used to populate the user interface of certain ASP.NET controls with child controls set out in accordance with a template. Figure 11 shows the most interesting part of the code; it creates an in-memory templated column for a DataGrid control. Incidentally, nearly identical code can be used for DataList and Repeater controls. The only difference is the type casting done in the OnDataBinding handlers. The structure of the class you have to write looks like: class LastFirstNameTemplate : ITemplate { public void InstantiateIn(Control container) {...} private void BindLastName(Object s, EventArgs e) {...} private void BindFirstName(Object s, EventArgs e) {...} }The class can be defined in the <script> section of an ASPX page as well as in separate class files. Another good place for this kind of code is in the code-behind file for ASPX pages. In the body of InstantiateIn, you simply create instances of controls and add them to the specified container. For DataGrid controls, the container is an object of type DataGridItem. It will be DataListItem for the DataList control. In general, a good container is any class that implements the INamingContainer interface. Label lblName = new Label(); lblName.DataBinding += new EventHandler(this.BindName); container.Controls.Add(lblName);If the control being added to the container's Controls collection has to be bound to one data source column, then you also register your own handler for the OnDataBinding event. When the event occurs, you retrieve the text from the data source and refresh the user interface of the control. When defined for a server control, the DataBinding event handler is expected to resolve all data binding expressions in the server control and in any of its children. void BindName(Object sender, EventArgs e) { Label l = (Label) sender; DataGridItem container; container = (DataGridItem) l.NamingContainer; DataRowView drv; drv = ((DataRowView) container.DataItem); l.Text = drv["lastname"].ToString(); }A DataBinding event handler has two key tasks to accomplish. First, it should get hold of the underlying data item. Second, it has to refresh the UI of the bound control to reflect the binding. A reference to the involved control can be obtained through the sender parameter. The container that hosts the control is returned by the NamingContainer property on the control. At this point, you have everything you need to set up and use another ASP.NET expression that should be familiar: Container.DataItem. The data item type depends on the data source associated with the DataGrid. It is DataRowView in most real-world cases. At this point you are just about finished. All that remains is to access a particular column on the row and set the control's bound properties. A Multi-select DataGrid To top off my discussion of templates in ASP.NET, let me illustrate a practical circumstance in which creating dynamic templates results in some pretty cool code—the multi-selection DataGrid control: <%@ Register TagPrefix="expo" Namespace="BWSLib" Assembly="MultiGrid" %> ••• <expo:MultiGrid id="grid" runat="server" AutoGenerateColumns="false" AllowMultiSelect="true" AllowMultiSelectFooter="true" font-size="x-small" font-names="verdana" BorderStyle="solid" BorderWidth="1" GridLines="both">It is a brand new control that inherits all the DataGrid functionalities and, moreover, can be instantiated and configured like a normal grid. Upon creation, this new control adds an extra column made of a checkbox to allow for selection. In addition, the footer is modified to accommodate for a link button that deselects all the checked rows. Figure 12 lists the new methods and properties defined for the MultiGrid control. Figure 13 shows the control in action in a sample ASP.NET page. Figure 13 The Control in Action Finally, in Figure 14 I've written an overview of the key parts of the control's code. This overview is based on a couple of ITemplate-based classes which provide for ItemTemplate and FooterTemplate properties. Conclusion Templates are the key to providing a graphical representation of data that makes sense to the user. In this column, I demonstrated how to get the most out of ASP.NET templates and a few ways to create and load them programmatically. I worked on the DataGrid control, but by renaming a few classes the same concepts can be applied to the DataList, the Repeater, and all custom and future controls based on templates. My book, Building Web Solutions with ASP.NET and ADO.NET, to be published early in 2002 by Microsoft Press, will provide more low-level, task-oriented information on templates. Send questions and comments for Dino to cutting@microsoft.com. |
Understanding Templates in ASP.NET
最新推荐文章于 2020-09-15 02:10:41 发布