Cutting Edge

Extending the GridView Control

Dino Esposito

Code download available at:CuttingEdge05.exe(132 KB)

Contents

The GridView Difference
A New GridView Control
Adding a Checkbox Column
The New Grid In Action
Styling Selected Rows with Script Code
Script Code Injection
Wrap-Up

Welcome to my100th consecutive installment of Cutting Edge. I've been writing this column since January 1998 in Microsoft Internet Developer. Looking back over the past eight years, I realize that I've touched on almost every subject in the Windows® SDK and the Microsoft® .NET Framework. In all that time, one topic has really stood out and kept you coming back for more: the DataGrid. So to celebrate my 100th column, I'm going to talk about the successor of the ASP.NET DataGrid control—the GridView. I've already provided some details in my August 2004 article "GridView: Move Over DataGrid, There's a New Grid in Town!". Now that ASP.NET 2.0 is released, I'll continue the discussion.

The GridView Difference

Both DataGrid and GridView are rich, sophisticated controls that provide a full bag of goodies on their own. In addition, both controls expose a rich, customizable infrastructure of properties, styles, templates, and a good number of overridable members. Both the DataGrid and GridView controls are designed from the ground up for extensibility. They feature a number of protected virtual methods that control developers can utilize to architect and build even more complex components specific to their applications.

In this installment of the column, I'll show how to implement a GridView-derived control that supports multiple selection. Along the way, you'll see internal members overridden, new members added, script code silently injected into the page, and a number of state management techniques.

I think it will be much easier for you to follow the development of this column if you know exactly where I'm taking you. The final destination will be a grid like the Hotmail® Inbox grid that is shown in Figure 1.

Figure 1 Hotmail Inbox Grid

Figure 1** Hotmail Inbox Grid **

As you can see the page displays e-mail messages in a grid that has sorting and filtering capabilities, along with links to images in some rows, which is not very difficult to accomplish. The checkbox column, however, fascinates me every time I check my Hotmail account.

In the grid, the checkbox column allows users to select multiple messages and execute on all of them any function that is available on the toolbar. As you probably know, DataGrid and GridView controls support selection, but only one item at a time can be selected. So how does Hotmail allow the selection of multiple messages?

To have a checkbox column in a grid, you don't need a custom control. In ASP.NET 2.0, you add a bound CheckBoxField to the GridView. If more flexibility is required, you add a templated column that includes a CheckBox control. As you'll see in a moment, the CheckBoxField control requires binding to a data source field and doesn't display any user interface elements if not bound. A templated column is the way to go if you plan to use the checkbox as a selection element. But my goal is different—I want multiple selection. When multiple selection is enabled the grid should automatically add a checkbox column whose position can be configured at design time as well as at run time.

As in the Hotmail grid, when the user checks an element, the whole row is rendered with a different style. The row returns to its original style when the element is unchecked. The operation is accomplished entirely on the client through script and doesn't require a postback. Finally, the checkbox column also contains a checkbox in the header cell. When the top element is checked or unchecked, all rows in the grid are set accordingly. This capability is also achieved through script and without round-trips to the server.

A New GridView Control

The new control will be named GridView but it will clearly belong to a different namespace—for example, MsdnMag.CuttingEdge.Controls, as shown here:

Namespace MsdnMag.CuttingEdge Public Partial Class GridView System.Web.UI.WebControls.GridView ... End Class End Namespace

Note the partial modifier on the class declaration. It is not strictly required from a syntax point of view, but it does come in handy to split the source code of large and complex controls into multiple files. As you can see from the companion code for this column, the new GridView control is divided into two source files—gridview.cs and gridview.helpers.cs. Both contribute a different part of the code, thus making the whole library project more manageable. Partial classes are a hot new feature of some of the compilers in the .NET Framework 2.0 and are not specific to ASP.NET.

The object model of the base GridView control is extended with two new properties: AutoGenerateCheckBoxColumn and CheckBoxColumnIndex (see Figure 2). AutoGenerateCheckBoxColumn is a Boolean property and indicates whether a checkbox column should be generated automatically at run time. CheckBoxColumnIndex indicates the (0-based) index of this new column.

Figure 2 Properties of the New GridView Control

<Category("Behavior")> _ <Description( _ "Whether a checkbox column is generated automatically at runtime")> _ <DefaultValue(False)> _ Public Property AutoGenerateCheckBoxColumn As Boolean Get Dim o As Object = ViewState("AutoGenerateCheckBoxColumn") If o Is Nothing Then Return False Return DirectCast(o, Boolean) End Get Set(ByVal value As Boolean) ViewState("AutoGenerateCheckBoxColumn") = value End Set End Property <Category("Behavior")> _ <Description("Indicates the 0-based position of the checkbox column")> _ <DefaultValue(0)> _ Public Property CheckBoxColumnIndex As Integer Get Dim o As Object = ViewState("CheckBoxColumnIndex") If o Is Nothing Then Return 0 Return DirectCast(o, Integer) End Get Set(ByVal value As Integer) ViewState("CheckBoxColumnIndex") = IIf(value < 0, 0, value) End Set End Property

When you create a custom control, the naming convention you use for properties is more important than you may realize. Using names that are like names of similar elements in other ASP.NET controls significantly helps other developers using your code.

In ASP.NET 2.0, the ListBox supports multiple selection (the other control to do so is the CheckBoxList). It features a SelectionMode property that takes values out of the ListSelectionMode enumeration—Single, Multiple. What's the best choice? Should you dovetail with the ListBox style or name properties after the GridView standard naming? I believe neither option is wrong here. My first impulse was to use a SelectionMode property. Later, I realized that the GridView, which has good support for single selection, doesn't have a property to explicitly enable selection. It does that by adding a select button column. In other words, for the GridView, the implementation detail—the button column—counts more than the functionality. Since I'm extending the GridView, I thought that an AutoGenerateCheckBoxColumn property would have been better.

Of course, coupling the auto-generation of the column with the concept of multiple selection may limit users. For instance, what if I want to enable multiple selection but I want to set styles on my checkbox column? Or what if I want to use radio buttons instead of checkboxes? I'd have to duplicate the multiple selection logic. All of the code for this column is available for download from the MSDN®Magazine Web site, and if you disagree with my decisions, you're welcome to edit the code to your heart's content.

Adding a Checkbox Column

The columns displayed by a grid control can be specified in two ways. You can statically indicate which columns you want to display through the <columns> tag, or you can charge the grid with the task of adding as many columns as there are fields in the bound data source. This latter approach is called autogeneration and is controlled by the AutoGenerateColumns property. The two approaches can be combined and you can have, for example, a grid that first autogenerates its column set and then adds a number of statically defined columns.

When it comes to adding columns to a grid dynamically, a key fact to bear in mind is that the Columns property of a GridView and the Columns property of the DataGrid don't contain the complete list of columns being displayed. If the controls' columns are autogenerated, Columns returns an empty collections. Otherwise, both properties only list statically defined columns.

The real column set for a grid control is determined by a method that is invoked during the rendering process. The method, named CreateColumns, is marked as protected and overridable. The method takes a couple of parameters, as you see here:

Protected Overridable Function CreateColumns( _ ByVal dataSource As PagedDataSource, _ ByVal useDataSource As Boolean) As ICollection

The first argument is an object of type PagedDataSource. PagedDataSource is a public type that wraps the bound data source. The second argument, useDataSource, is a Boolean value that indicates whether the column set should be created from the data source (that is, through autogeneration). The CreateColumns method returns a collection formed by DataControlField objects. The useDataSource argument is only looked at if AutoGenerateColumns is true. If it is, if useDataSource is also true, then it uses the data source to create the autogenerated columns. Otherwise, it uses view state properties to create the autogenerated columns.

To add a new column, you override the CreateColumns method, as shown in Figure 3. The overridden version of CreateColumns first calls the base method and gets the default collection of columns. Next, if the checkbox column is required, it adds a new DataControlField to the collection and returns. Note that the base CreateColumns method returns an object that implements ICollection. What's ICollection exactly? It is an interface defined in the following manner:

Public Interface ICollection Implements IEnumerable Sub CopyTo(ByVal array As Array, ByVal index As Integer) ReadOnly Property Count As Integer ReadOnly Property IsSynchronized As Boolean ReadOnly Property SyncRoot As Object End Interface

As you can see, the ICollection interface doesn't provide any methods to add new items. To insert a new column into the list, you need to copy the ICollection object into an ArrayList, or a similar ICollection-based object that allows you to add items to the collection, and then proceed with required changes. Figure 4 shows the source code of the AddCheckBoxColumn method where the ICollection object is copied into an ArrayList and then extended with a newly created field.

Figure 4 Adding a New Checkbox Column to the Grid

Protected Overridable Function AddCheckBoxColumn( _ ByVal columnList As ICollection) As ArrayList ' Get a new container of type ArrayList that contains the ' given collection. This is required because ICollection ' doesn't include Add methods. Dim list As New ArrayList(columnList) ' Create a new custom CheckBoxField object Dim field As New InputCheckBoxField field.HeaderText = "<input type=checkbox>" field.ReadOnly = True ' Insert the checkbox field into the list at the specified position If CheckBoxColumnIndex > list.Count Then list.Add(field) CheckBoxColumnIndex = (list.Count - 1) Return list End If list.Insert(CheckBoxColumnIndex, field) Return list End Function

Figure 3 Override the CreateColumns Method

Protected Overridable Function CreateColumns( _ ByVal dataSource As PagedDataSource, _ ByVal useDataSource As Boolean) As ICollection ' Let GridView create the default set of columns Dim list as ICollection = _ MyBase.CreateColumns(dataSource, useDataSource) If Not AutoGenerateCheckBoxColumn Then Return list ' Add checkbox column if required Return AddCheckBoxColumn(list) End Function

The new field will be inserted in the position specified by the CheckBoxColumnIndex property. When you set the property, any negative value is automatically evaluated as zero, as implemented in Figure 2. The CheckBoxColumnIndex property setter, though, can't check the number of available columns to set an upper bound for the value. This check is performed when the column is physically added to the list, that is, as soon as the real number of columns being displayed is known.

As you see in Figure 4, a custom data control class is used to display a checkbox in the column. ASP.NET 2.0 has a brand new CheckBoxField class that seems perfect, so why not use it instead? The problem is that CheckBoxField puts a checkbox it each cell only if it is bound to a valid data source field. In this case, you do want a checkbox in each column, but you also need to keep the column unbound and use it only to collect some input. For this task, a CheckBoxField component is simply not appropriate. In Figure 5 you see the source code of a replacement class—InputCheckBoxField. The class is marked internal and sealed because its usefulness is limited to the implementation of this enhanced GridView control.

Figure 5 InputCheckBoxField Class

Namespace MsdnMag.CuttingEdge.Controls Friend NotInheritable Class InputCheckBoxField : Inherits CheckBoxField Public Sub New() End Sub Public Const CheckBoxID As String = "CheckBoxButton" Protected Overrides Sub InitializeDataCell( _ ByVal cell As DataControlFieldCell, _ ByVal rowState As DataControlRowState) MyBase.InitializeDataCell(cell, rowState) ' Add a checkbox anyway, if not done already If (cell.Controls.Count = 0) Then Dim chk As New CheckBox chk.ID = InputCheckBoxField.CheckBoxID cell.Controls.Add(chk) End If End Sub End Class End Namespace

The InputCheckBoxField class overrides the InitializeDataCell method, which is the nerve centre of a grid column class where the UI of a cell is determined. The overridden method calls the base version and then, if no controls have been created, adds an unbound checkbox with a predefined ID. You're pretty much finished with the core functionality—checking multiple rows simultaneously. Before putting a sample page through its paces, I'll briefly discuss the reasons why the ASP.NET team decided to expose the column set through a collection class instead of a list class.

When writing a public API that includes a list of objects as input or output, should you use a collection or a list? Here a collection is a class that implements ICollection, whereas a list is a class that implements IList. In general, you should use a collection for class properties and for return values of methods. A list is more appropriate for implementing internal properties or for specifying the input value of a property or the argument of a method. The CreateColumns method returns ICollection in accordance with this guideline.

IList has a richer set of methods than ICollection and, more importantly, has members that can add or remove items. Because IList implements ICollection, you can use a list as an input value wherever an ICollection is accepted.

What if you really need the extra methods of a list like in the override of the CreateColumns method that you saw in Figure 3? In this case, it is recommended that you copy your nonextensible collection into a list object such as an ArrayList or a List(Of T) and proceed from there. An interesting blog that covers these topics is written by Krzysztof Cwalina, a program manager on the common language runtime (CLR) team at Microsoft.

The New Grid In Action

Let's create a page that contains my custom GridView. It will provide a simple and effective toolbar to expose any functions to call on the selected items. The markup for the grid is nearly identical to the markup of a regular GridView, except for the new properties AutoGenerateCheckBoxColumn and CheckBoxColumnIndex. Figure 6 shows the page. Users can click on the checkboxes and thus mark the row for selection. I'll discuss in a moment how to enhance the code to have each selected row rendered with a different style.

Figure 6 New GridView Control

Figure 6** New GridView Control **

So you now have a multiselection control. How do you know which rows are currently selected? Imagine the user clicks on a page button or a menu item, and the page posts back needing to execute some special code for each selected item. How would you work that out?

The ListBox control, another ASP.NET control to support multiple selection, exposes a public method named GetSelectedIndices. This method returns an array of integers where each integer represents the 0-based index of a selected element:

Public Overridable Function GetSelectedIndices() As Integer() Return CType(SelectedIndices.ToArray( _ GetType(Integer)), Integer()) End Function

The method wraps an internal property that contains all selected indices. The heart of the function is just inside the implementation of the property in Figure 7.

Figure 7 SelectedIndices

Friend Overridable ReadOnly Property SelectedIndices As ArrayList Get Dim indices as New ArrayList For i As Integer = 0 To Rows.Count - 1 Dim cb As CheckBox cb = CType(Rows(i).FindControl( InputCheckBoxField.CheckBoxID), CheckBox) If cb Is Nothing Then Return indices If cb.Checked Then indices.Add(i) Next Return indices End Get End Property

The property is protected and as such doesn't show up in the Visual Studio 2005 IDE. It is a read-only property that works by copying the indices of the selected row into a local array list object. The get accessor of the SelectedIndices internal property loops through the Rows collection of the grid and retrieves the instance of the checkbox. If the checkbox is checked, the index is added to the array.

From a codebehind class, you consume the list of selected indices as follows:

For Each index As Integer in _ GridView1.SelectedIndices Dim key As String = _ GridView1.DataKeys(index).Value.ToString() ... Next

As long as you're interested in the plain index, the array returned by SelectedIndices is perfect. Unfortunately, you usually need a bit more than the index. Typically, you need a key value for the selected row that allows you to easily and effectively retrieve the data item associated with it. For example, if the grid displays a list of employees, the key value is typically the employee ID. The GridView allows you to define multiple data key fields through the DataKeyNames property.

Upon postback, the values that are effective for the specified fields are available in the DataKeys array. By using a returned index as a selector in this array, you easily obtain key information for the selected rows:

For Each index As Integer in GridView1.SelectedIndices Dim key As String = GridView1.DataKeys[index].Value.ToString() ... Next

Styling Selected Rows with Script Code

As I mentioned, the GridView control fully supports single-row selection. It does that through the SelectedIndex and the SelectedRowStyle properties. Once you set the SelectedIndex property to an index, the control renders the corresponding row according to the SelectedRowStyle style attributes. Another property, called AutoGenerateSelectButton, allows you to add a column of select buttons. Clicking any button automatically sets the SelectedIndex property to the index of the clicked row. Note that this clicking causes a postback.

When you click on a checkbox element instead, no round-trip occurs. Would it be possible to update the style of the clicked row on the client and persist these changes to the server on the next postback? Fortunately, ASP.NET takes care of most of the burden by automatically persisting the checked state of each checkbox in the column. In Figure 6, in fact, you see a message obtained by processing the selection. This fact clearly indicates that a postback occurred. At the same time, the checkboxes mentioned in the message are still checked.

You need to add some script code to each checkbox so that the style of the row changes in accordance with the checked state of the checkbox. To keep client-side events in sync with server-side events, you might want to use the same visual attributes of SelectedRowStyle on the client. Figure 8 shows a modified version of the control that uses client-side script to style selected rows. As you can guess from the figure, if you write good, cross-browser script code, the feature will work on most browsers available today, including Firefox.

Figure 8 Cross-Browser Compatibility

Figure 8** Cross-Browser Compatibility **

You need a JavaScript onclick event to be associated with each checkbox displayed in the column. The onclick event handler will simply set some style attributes. Figure 9 details the JavaScript code injected by the GridView control into each page that uses it.

Figure 9 Client-Side Styling of the GridView

// Invoked to check/uncheck all when header checkbox is clicked // --------------------------------------------------------------- function CheckAll(me) { // Assume the ID of this element is controlID_HeaderButton // (i.e. GridView1_HeaderButton) var index = me.name.indexOf('_'); var prefix = me.name.substr(0,index); // Looks for the right checkbox for(i=0; i<document.forms[0].length; i++) { var o = document.forms[0][i]; if (o.type == 'checkbox') { if (me.name != o.name) { if (o.name.substring(0, prefix.length) == prefix) { // Needs to set the state of the header to any child // checkbox. To avoid passing too many parameters, // sets state by simulating the click. To ensure that // the click sets the checkbox to the "me.checked" // state, it must first the current value. o.checked = !me.checked; o.click(); } } } } } // Invoked to properly style the checkbox on a row // --------------------------------------------------------- function ApplyStyle(me, selectedForeColor, selectedBackColor, foreColor, backColor, bold, checkBoxHeaderId) { var td = me.parentNode; if (td == null) return; var tr = td.parentNode; if (me.checked) { tr.style.fontWeight = 700; tr.style.color = selectedForeColor; tr.style.backgroundColor = selectedBackColor; } else { document.getElementById(checkBoxHeaderId).checked = false; tr.style.fontWeight = bold; tr.style.color = foreColor; tr.style.backgroundColor = backColor; } }

Figure 10 illustrates the server code that runs in the pre-render phase and injects any needed onclick handlers. The ApplyStyle function is associated with each checkbox in the column and sets font and color on the row. The function takes six parameters—foreground and background colors to use when the row is selected and unselected, plus a value that indicates whether a boldface font is required. The final argument is the ID of the checkbox in the header. (I'll return to this in a moment.)

Figure 10 Supporting Client Scripting

Protected Overrides Sub OnPreRender(ByVal e As EventArgs) ' First do as usual MyBase.OnPreRender(e) ' Adjust each data row Dim row As GridViewRow For Each row In Row ' Get the appropriate style object for the row Dim style As TableItemStyle = GetRowStyleFromState(row.RowState) ' Retrieve the reference to the checkbox Dim cb As CheckBox = CType(row.FindControl( _ InputCheckBoxField.CheckBoxID), CheckBox) ' Build the ID of the checkbox in the header Dim headerCheckBoxID As String = _ String.Format("{0}_HeaderButton", ClientID) ' Add script code to enable selection cb.Attributes.Item("onclick") = String.Format( _ "ApplyStyle(this, '{0}', '{1}', '{2}', '{3}', '{4}', '{5}')", ColorTranslator.ToHtml(SelectedRowStyle.ForeColor), ColorTranslator.ToHtml(SelectedRowStyle.BackColor), ColorTranslator.ToHtml(style.ForeColor), ColorTranslator.ToHtml(style.BackColor), IIf(style1.Font.Bold, 700, 400), headerCheckBoxID) If cb.Checked Then row.BackColor = SelectedRowStyle.BackColor row.ForeColor = SelectedRowStyle.ForeColor row.Font.Bold = SelectedRowStyle.Font.Bold Else row.BackColor = style.BackColor row.ForeColor = style.ForeColor row.Font.Bold = style.Font.Bold End If Next End Sub

The colors for the selected state are taken directly from the SelectedRowStyle object. The SelectedRowStyle is just part of the style applied to a selected row. RowStyle and AlternatingRowStyle will also apply for style properties that aren't set in SelectedRowStyle. You can think of the applied style as a layering of RowStyle, those that apply of AlternatingRowStyle and EditRowStyle, and finally SelectedRowStyle. This means that a classic select button row will look different than a new multiselect row.

The header of the checkbox column also shows a checkbox. The idea is to use that checkbox to check or uncheck all rows. To begin with, the header text of the checkbox column should be changed to a more sophisticated template, like this:

<input type='checkbox' id='{0}' name='{0}' {1} onclick='CheckAll(this)'>

You must give the checkbox a unique ID that must be known to other portions of code in the control. This ID can be considered a private constant. I arbitrarily named it XXX_HeaderButton where XXX stands for the ID of the grid control.

When the user clicks the header checkbox, the CheckAll script function simulates the click on all checkboxes in the column. Given this expected behavior, it follows that anytime you uncheck an element in the column, you must also ensure that the header checkbox is unchecked. For this reason, the ApplyStyle function needs to know the ID of the header checkbox (see Figure 9).

There's one little feature left. The state of the header checkbox is not automatically persisted across postbacks. This is due to the fact that you are forced to add the checkbox via text. In ASP.NET, in fact, you can only use a string to set the header of a column unless you use a template field.

In any case, the browser posts a field with the client-side state of the header checkbox. Because there's no server-side control in the page where ID matches the XXX_HeaderButton name, that value is lost. How can you retrieve that? Simply by using the old faithful Page.Request object. By adding the following code to the AddCheckBoxColumn method you saw earlier, you insert a key "checked" attribute in the HTML string that renders out the checkbox in the header:

Dim shouldCheck As String = String.Empty Dim checkBoxID As String = String.Format("{0}_HeaderButton", ClientID) If Not DesignMode Then Dim o As Object = Page.Request(checkBoxID) If o IsNot Nothing Then shouldCheck = "checked='checked'" End If

Note that the custom GridView control takes full advantage of the design-time infrastructure of the parent GridView control. You don't need to add any design-time specific feature, except perhaps for some property descriptions. However, the AddCheckBoxColumn method is invoked when the control renders out. This means you will get an error in Visual Studio 2005 because Page.Request is not defined at design time. On the other hand, at design time you don't need to know about the state of the header checkbox and can simply render it always unchecked. The new DesignMode property that is found in all ASP.NET 2.0 controls lets you fix issues like these throughout the control code.

An alternative solution is to have InputCheckBoxField override InitializeCell to look for header cells, put a checkbox in each, and give each checkbox in a header the ID you want, making it easier to find later. That would avoid this whole issue.

Script Code Injection

So the multiselection grid requires some script code to be injected in the host page. Pasting the code from Figure 9 in every page where you use the grid control is clearly not an option. A better option would be to save the code to a .js file to include in each host page. In both cases, though, you have a tight coupling between the script and the page. It would be much better if the control itself could automatically inject the code in each page where it is hosted.

ASP.NET provides a number of methods for registering code with the .NET Framework that will be emitted in the markup served to the browser. The set of methods in ASP.NET 2.0 is rooted in the ClientScript object. RegisterClientScriptInclude, for example, emits a <script> tag with a src attribute. The great news is that in ASP.NET 2.0 you can embed a .js file in the assembly and expose it as a Web resource.

You take the .js file that contains the source code to inject and add it to a control project as an embedded resource. It is important that you change the build action of Visual Studio 2005 for this file from Content (the default) to Embedded Resource. In this way, the .js file becomes part of the assembly and is deployed along with the control. The following code shows how to register a <script> block bound to an autogenerated URL that refers to the script in the assembly:

Dim t As Type = Me.GetType() Dim js As String = "MsdnMag.CuttingEdge.Controls.HotGridView.js" Dim url As String = Page.ClientScript.GetWebResourceUrl(t, js) Page.ClientScript.RegisterClientScriptInclude(t, js, url)

GetWebResourceUrl returns a URL for the assembly resource. The returned URL is based on webresource.axd, a new system HTTP handler defined in ASP.NET 2.0. It is important that the resource name includes namespace information and that the resource is decorated with the WebResourceAttribute. You do that in the AssemblyInfo.vb file through the following code:

<assembly: WebResource("MsdnMag.CuttingEdge.Controls.HotGridView.js", _ "application/x-javascript")>

Wrap-Up

Although the GridView is a rich and powerful control, there are still changes you can make to improve it. The new grid control I created here, for example, works like the parent GridView control and supports multiple selection through a checkbox column with client-side capabilities.

When it comes to extending the feature set of an existing control, what you can do mostly depends on how the extendee control was designed. Without a virtual CreateColumns method, for example, it would have been much harder, if not impossible, to create a new grid control as I did here. When you create your own controls, think about ways that other developers might want to customize and extend them.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino by e-mailing him at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.