Friday, December 7, 2007

Server Controls and writing ASP.Net


Server Controls and Validation


We have already used server controls in many of the examples of building ASP.NET pages in previous chapters. In this chapter and the two that follow, we are going to be looking in more depth at exactly what server controls are and how we can use them. In fact, we will be examining all the different types of server controls that are supplied with the standard .NET installation.

Server controls are at the heart of the new ASP.NET techniques for building interactive web forms and web pages. They allow us to adopt a programming model based on server-side event handling that is much more like the structured event-driven approach we are used to when building traditional executable programs.

Of course, as the .NET framework is completely extensible, we can build our own server controls as well, or just inherit from existing ones and extend their behavior. We will look at how we can go about building our own server controls later in this book. In the meantime, we will stick to those that come as part of the standard .NET package.

The topics we will cover in this chapter are:

* What are server controls.
* How to build interactive forms and pages using them.
* The server controls supplied with .NET.
* A detailed look at the HTML and Input Validation controls

writing ASP.Net

You have already taken a high-level look at the .NET Framework, and seen a few quick examples of ASP.NET pages, so now let's dive in and look in more details at how to create ASP.NET pages. Whether they are called ASP.NET pages or Web forms, these files form the core of all ASP.NET applications.

* The old way of creating ASP pages versus the new way with ASP.NET.
* The steps a page goes through as it is processed.
* How to use the various features of the Page object.
* Breaking up a page into reusable objects called user controls.

ASP.NET Web Form Controls

The basic concept of using server controls allows the change to a more structured event-driven-programming model. This provides a cleaner programming environment that is easier to work in and debug when things go wrong.

  • The ASP.NET Web Form controls in general.

  • The basic Web Form input and navigation server controls.

  • The Web Form server controls used for building lists.

  • The 'rich' Web Form controls that provide complex compound interface elements

The WebControl Base Class

Like the HTML controls, most Web Form controls inherit their members (properties, method, and events) from a base class. In this case, it is WebControl, defined within the System.Web.UI.WebControls namespace. This class provides a wide range of members, many of which are only really useful if you are building your own controls that inherit from WebControl. The public members used most often are shown in the following table:

Member

Description

Attributes property

Returns a collection of all the attribute name/value pairs within the .aspx file for this control. Can be used to read and set non-standard attributes (custom attributes that are not actually part of HTML) or to access attributes where the control does not provide a specific property for that purpose.

AccessKey property

Sets or returns the keyboard shortcut key that moves the input focus to the control.

BackColor property

Sets or returns the background color of the control.

BorderColor property

Sets or returns the border color of the control.

BorderStyle property

Sets or returns the style of border for the control, in other words, solid, dotted, double, and so on.

BorderWidth property

Sets or returns the width of the control border.

ClientID property

Returns the control identifier that is generated by ASP.NET.

Controls property

Returns a ControlCollection object containing references to all the child controls for this control within the page hierarchy.

Enabled property

Sets or returns a Boolean value indicating whether the control is enabled.

EnableViewState property

Sets or returns a Boolean value indicating if the control should maintain its viewstate and the viewstate of any child controls when the current page request ends.

Font property

Returns information about the font used in the control.

ForeColor property

Sets or returns the foreground color used in the control, usually the color of the text.

Height property

Sets or returns the overall height of the control.

ID property

Sets or returns the identifier specified for the control.

Page property

Returns a reference to the Page object containing the control.

Parent property

Returns a reference to the parent of this control within the page hierarchy.

Style property

References a collection of all the CSS style properties (selectors) that apply to the control.

TabIndex property

Sets or returns the position of the control within the tab order of the page.

ToolTip property

Sets or returns the pop-up text displayed when the mouse pointer is over the control.

Visible property

Sets or returns a Boolean value indicating whether the control should be rendered in the page output.

Width property

Sets or returns the overall width of the control.

DataBind() method

Causes data binding to occur for the control and all its child controls.

FindControl() method

Searches within the current container for a specified server control.

HasControls() method

Returns a Boolean value indicating whether the control contains any child controls.

DataBinding() event

Occurs when the control is being bound to a data source.

The Specific Web Form Control Classes

Each of the Web Form controls inherits from WebControl (or from another control that itself inherits from WebControl), and adds its own task-specific properties, methods, and events. Those commonly used (for each control) are listed in the following table:

Control

Properties

Events

HyperLink

ImageUrl, NavigateUrl, Target, Text

- none -

LinkButton

CommandArgument, CommandName, Text, CausesValidation

OnClick(), OnCommand()

Image

AlternateText, ImageAlign, ImageUrl

- none -

Panel

BackImageUrl, HorizontalAlign, Wrap

- none -

Label

Text

- none -

Button

CommandArgument, CommandName, Text, CausesValidation

OnClick(), OnCommand()

TextBox

AutoPostBack, Columns, MaxLength, ReadOnly, Rows, Text, TextMode, Wrap

OnTextChanged()

CheckBox

AutoPostBack, Checked, Text, TextAlign

OnCheckedChanged()

RadioButton

AutoPostBack, Checked, GroupName, Text, TextAlign

OnCheckedChanged()

ImageButton

CommandArgument, CommandName, CausesValidation

OnClick(), OnCommand()

Table

BackImageUrl, CellPadding, CellSpacing, GridLines, HorizontalAlign, Rows

- none -

TableRow

Cells, HorizontalAlign, VerticalAlign

- none -

TableCell

ColumnSpan, HorizontalAlign, RowSpan, Text, VerticalAlign, Wrap

- none -

Literal

Text

- none -

PlaceHolder

- none -

- none -


.NET Components

Writing Business Objects

First, let's look at creating business objects that can be used by .NET applications. These business objects will perform the same types of functions that business objects in COM or other objects models do, plus they will be able to make use of all of the advantages offered by the .NET architecture and the CLR.

This section looks at how to create an object and then extend that object through inheritance. Then it looks at how to extend the functionality of an existing class and also how to utilize some of the COM+ component services within the .NET object. After creating the object, you compile it and place it in an assembly. Once the assembly is created, you will create an ASP.NET page to test the new object.

There are two concurrent examples through the chapter. They share exactly the same functionality, except that one is written in Visual Basic .NET and the other is written in C#. These objects will be tested from an ASP.NET page, but could just as easily be tested from a Windows Forms application or from a command line application. Use a simple text editor to create the files, and the command line compilers and tools to create the assemblies.

Building the Object

To create the object, you need to look at the:

  • Guidelines for creating a component

  • Attributes that can be set to describe a component

With all of that out of the way, you can move to actually creating the example objects.

Class Design Guidelines

When writing components for .NET, some of the existing component design guidelines can still be used. However, just as the .NET Framework is different from COM and COM+, some of the design guidelines you have used in the past are now implemented in a different way. In the past, there may have even been different guidelines depending on the language used. Now with .NET, those guidelines are unified. One of the other keys to using these design guidelines is that they have been adhered to by Microsoft in the creation of the System Frameworks themselves, along with all of the sample code that comes with the SDK.

Error Handling

Now that robust error handling (including structured exception handling) is part of the CLR, and therefore available to all languages supported by the CLR, you should use it wherever possible. The former practice of using error codes and checking return values or even On Error Goto has been replaced with catching exceptions and handling the errors at that point.

This doesn't mean you should use exceptions everywhere – exceptions are designed to handle errors, not something you should expect to happen. However, there are instances where error codes can come in handy. For example, if you are trying to open a file and the file doesn't exist, return a null value, since that error could be expected in normal use. But if the file system returns an I/O error, throw an exception, since that condition isn't normally expected.

Properties versus Methods

One of the most difficult choices in designing a component is choosing what type of interface to use. This holds true for all component-based architectures, not just .NET. Knowing when to use a property as opposed to a method and vice versa, is as much a matter of personal taste as are the following of design guidelines. The basic guidelines to follow are:

  • If there is an internal data member being exposed outside the component, use a property.

  • If the execution of the code causes some measurable side effect to the component or the environment, use a method.

  • If the order of code execution is important, use a method. Since the CLR has the ability to short- circuit expression testing, a property may not be accessed when you expect it will. Let's look at an example of a short-circuited expression.

    An object X has two properties, A and B. These properties do more than just expose an internal data member–they actually do some work as they return a value. For this example, they each return an integer between 1 and 10. They are being used in code that looks like this:

    if (X.A > 5) AndAlso (X.B < 7) then
    ... ' do something
    end if

    If the evaluation of X.A returns a 4, then the first part of the Boolean expression is False. In order for an AndAlso statement to be True, both parts have to be True. The CLR knows this too, and seeing that the first part is False, it will skip or short-circuit the evaluation of X.B, since its value doesn't matter. However, because the code violated good design principles and did work during the evaluation of B, that work will not be performed in this case. This is probably not the desired effect.

Memory Management

Memory management is one of the most difficult things that most programmers face. Now that the operating system has gone to a flat memory model, developers don't have the issues from the days of Windows 3.1 about allocating memory. However, they still have had to deal with how and when to discard the memory. With the CLR handling most of the memory management for .NET components, there are only a few things that developers need to do differently when dealing with memory than in the past.

The CLR has the ability to create small, short-lived objects very quickly and cheaply. Thus you shouldn't be worried about creating objects that make the development easier to follow. According to performance testing done by Microsoft, the runtime can allocate nearly 10 million objects per second on a moderately fast machine. Also, objects running in the CLR will be garbage-collected by the runtime after they are no longer being referenced. This happens automatically, and keeps the developer from having to deal with memory leaks from improperly-freed objects. While automatic garbage collection does deal with a lot of headaches, there still have to be processor cycles dedicated to the garbage collector running. When it actually runs is also unpredictable, and could cause a temporary hiccup in performance.

Using Attributes

Attributes in the CLR allow developers to add additional information to the classes they have created. These are then available to the application using the component through the System.Reflection classes. You can use attributes to provide hints or flags to a number of different systems that may be using the component. Attributes can be used as compiler flags to tell the compiler how to handle the compilation of the class. They can be used by tools to provide more information about the usage of the component at design-time. This means developers can get away from having to embed comments in code simply as clues for the tool to know where certain parts of the code are. Attributes can also be used to identify the transaction characteristics of a class when interacting with the Components Services feature of the operating system.

The following two tables show the standard attributes for properties and events that are defined in the System.ComponentModel namespace in the CLR. As these attributes are already defined, they can be used by the developer without having to create a corresponding class for a custom attribute.

Here are the attributes common to events and properties:

Attribute

Description

Usage – default in bold

Browsable

Declares if this property should appear in the property window of a design tool.

[Browsable (false | true)]

Category

Used to group properties when being displayed by a design tool.

[Category (categoryName)]

Description

Help text displayed by the design tool when this property or event is selected.

[Description (descriptionString)]

The attributes for properties are as follows:

Attribute

Description

Usage – default in bold

Bindable

Declares if data should be bound to this property.

[Bindable (false | true)]

DefaultProperty

Indicates that this is the default property for the class.

[DefaultProperty]

DefaultValue

Sets a default value for the property.

[DefaultValue (value)]

Localizable

Indicates that a property can be localized. The compiler will cause all properties with this attribute to store the property in a resource file. The resources file can then be localized without having to modify any code.

[Localizable (false | true)]

The attribute for events is:

Attribute

Description

Usage

DefaultEvent

Specifies the default event for the class.

[DefaultEvent]

Sample Object

In this section you will see how to create a sample object,which will be used to encapsulate business and data access functionality – this is the typical usage for objects in the applications that most developers are creating. The business object will encapsulate the interaction with the IBuyAdventure database that is used in the case study discussed in Chapter 24. Since this is an example of how to build components rather than a full case study on a business and data component, the component will have limited functionality.

The component will have one property:

Property

Type

Usage

DatabaseConnection

String

The database connection string

The component will have three methods:

Method

Returns

Parameters

Usage

GetProductTypes

String Collection

none

Returns a string collection of all of the product types in the database.

GetProducts

DataSet

productType

Returns a DataSet containing the records for a specific product type.

AveragePrice

Single

productType

Returns a single value that represents the average price of the items of a specific product type.

With the interface defined, it is now time to write the component. As stated earlier, the component will be developed in both Visual Basic .NET and in C#. Let's start with the Visual Basic .NET version.

Visual Basic .NET Class Example

Here is how the final class looks when written in Visual Basic .NET:

Option Explicit
Option Strict

Imports System
Imports System.Data
Imports System.Data.SqlClient

Namespace BusObjectVB

Public Class IBAProducts

Private m_DSN As String

Public Sub New ()
MyBase.New
m_DSN = ""
End Sub

Public Sub New(DSN As string)
MyBase.New
m_DSN = DSN
End Sub

Public Property DatabaseConnection As string
Set
m_DSN = value
End Set
Get
Return m_DSN
End Get
End Property

Public Function GetProductTypes () As DataSet
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)
Dim sqlAdapter1 As New SqlDataAdapter("SELECT DISTINCT ProductType " _
& "FROM Products", myConnection)

Dim types As New DataSet()
sqlAdapter1.Fill(types, "ProdTypes")

Return types
End Function

Public Function GetProducts ( productType As String) As DataSet
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)
Dim sqlAdapter1 As New SqlDataAdapter("SELECT * FROM Products WHERE " _

& "ProductType='" & productType & "'", myConnection)

Dim products As New DataSet()
sqlAdapter1.Fill(products, "products")

Return products
End Function

Public Function AveragePrice ( productType As string) As Double
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)

Dim sqlAdapter1 As New SqlDataAdapter("SELECT AVG(UnitPrice) AS " _
& "AveragePrice FROM Products WHERE " _
& "ProductType='"+productType+"'", myConnection)

Dim AvgPrice As New DataSet()
sqlAdapter1.Fill(AvgPrice, "AveragePrice")

Dim priceTable As DataTable
priceTable = AvgPrice.Tables("AveragePrice")
If (Not priceTable.Rows(0).IsNull("AveragePrice")) Then
Return CDbl(priceTable.Rows(0)("AveragePrice"))
Else
Return 0
End If
End Function
End Class
End Namespace

Let's break down each part and describe what it does and how.

Look at the object in detail. The first two statements are unique to Visual Basic. With its roots as a loosely-typed language, Visual Basic .NET has had some directives added to it to tell the compiler that it should do some level of type checking when compiling the application. The Option Explicit statement is familiar to Visual Basic programmers. It forces the declaration of all variables before they are used, and will generate a compiler error if a variable is used before it is declared. The Option Strict statement is introduced with Visual Basic .NET. It greatly limits the implicit data type conversions that Visual Basic has been able to do in the past. Option Strict also disallows any late binding. This will increase the performance in components since types are checked at compile-time, and not at runtime.

Option Explicit
Option Strict

The next section states which parts of the System Frameworks will be used in this object:

Imports System
Imports System.Data
Imports System.Data.SqlClient

You can actually use any part of the System Frameworks at any time in the code by simply referencing the full path to it – System.Data.SqlClient.DataTable – but that would begin to make the code cumbersome and unnecessarily long. By explicitly stating which parts of the System Frameworks the component will use, you can refer to the particular class without having to state the full path – DataTable – as shown in the example.

This object uses the System namespace, which contains the necessary base classes to build the object. The System.Data namespace contains the classes that make up the ADO.NET data access architecture. Since the component accesses data in a SQL Server 2000 database, the component also includes the System.Data.SqlClient namespace. This namespace contains the classes to access the SQL Server-managed provider.

The object will be encapsulated in its own unique namespace, BusObjectVB, so first declare all of the classes that make up the object within that namespace. The business component is defined as a class – after creating an instance of it in the program it will then be an object:

Namespace BusObjectVB

Public Class IBAProducts

Within the object, there will be one private variable, which will be used to hold the database connection string. The next two methods are the constructors for the class. The constructor is automatically called by the runtime when an object is instantiated. There are actually two constructors. The first one takes no parameters and is therefore called the default constructor:

Private m_DSN As String

Public Sub New ()
MyBase.New
m_DSN = ""
End Sub

The second constructor takes a parameter, DSN, and will set the database connection string at the same time as the object is created:

Public Sub New(DSN As string)
MyBase.New
m_DSN = DSN
End Sub

Since a constructor cannot return any values, it is declared as a Sub rather than a Function. In Visual Basic, you must explicitly call the constructor for the base class, using the MyBase.New statement.

While the second constructor sets the database connection string when the object is created, the object needs to provide a way to set and read it at other times. Since the member variable holding this data is marked as private, there is a property function to set and retrieve the value. The external name of the property is DatabaseConnection:

Public Property DatabaseConnection As String
Set
m_DSN = value
End Set
Get
Return m_DSN
End Get
End Property

Next, look at the methods that work with the information in the database.

The first method, GetProductTypes, will retrieve a listing of the product types for the products stored in the database. This will return the listing to the calling program in a DataSet. A DataSet represents an in-memory cache of data. This means that it is a copy of the data in the database, so there is no underlying connection to the stored data. To access the database, you first need to connect to it. The SqlConnection object provides this functionality and when the Open method is called, it will connect to the database using the connection string that was stored in the private member variable m_DSN.

It is therefore important that this value be set properly. If the user of the component does not set the value of the DatabaseConnection property, the object won't be able to open the database. The best way to indicate this is to throw an exception. The object uses the Throw statement and passes it an instance of the ArgumentNullException class. This version of the constructor for this class takes two strings–the parameter that was Null and a text description of the error:

Public Function GetProductTypes () As DataSet
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)

To retrieve the desired information from the database, use a SQL query. To process the query, use the SqlDataAdapter. When you create the object, pass in the text of the SQL query that will be executed by this object. Also, tell the object which database connection object to use to access the data. That is the object that was created in the previous steps. The creation of this object will automatically open the database connection:

Dim sqlAdapter1 As New SqlDataAdapter("SELECT DISTINCT ProductType " _
& "FROM Products", myConnection)

With the mechanism for retrieving the data from the database, you need a place to store it to pass it back to the caller. This will be in a DataSet object. Create a new instance of this class and call it types. The data will be placed in this object by using the Fill method of the SqlDataAdapter class. This method takes the destination DataSet object as well as a name to represent the data within the data set. To send the data back to the caller, return the DataSet object types:

   Dim types As New DataSet()
sqlAdapter1.Fill(types, "ProdTypes")

Return types
End Function

The next method, GetProducts, shown in the following code will retrieve the list of products for a specified product type. Specify the product type by passing in the product type string. The list of products will be returned as a DataSet. The main part of the method is the same as the previous method; connect to the database and fill up a DataSet object with the information needed:

Public Function GetProducts ( productType As String) As DataSet
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)
Dim sqlAdapter1 As New SqlDataAdapter("SELECT * FROM Products WHERE " _
& "ProductType='" & productType & "'", myConnection)

Dim products As New DataSet()
sqlAdapter1.Fill(products, "products")

Return products
End Function

The resulting filled DataSet object can then be returned to the calling application.

Next, look at the method to calculate the average selling price of the items of a particular type:

Public Function AveragePrice ( productType As string) As Double
If m_DSN = "" Then
Throw(New ArgumentNullException("DatabaseConnection", _
"No value for the database connection string"))
End If
Dim myConnection As New SqlConnection(m_DSN)

Dim sqlAdapter1 As New SqlDataAdapter("SELECT AVG(UnitPrice) AS " _
& "AveragePrice FROM Products WHERE " _
& "ProductType='"+productType+"'", myConnection)

The method to calculate the average selling price for a product type will pass that value back as a return value of type double. Just as with the previous method, the one parameter for this method will be the product type of the desired product group. The database access code is again very similar – the primary difference being that the SQL statement calculates an average rather than returning a set of rows from the database:

Dim AvgPrice As New DataSet()
sqlAdapter1.Fill(AvgPrice, "AveragePrice")

Dim priceTable As DataTable
priceTable = AvgPrice.Tables("AveragePrice")

With the results of the database query in the DataSet object, take a look at the contents of the data to see what the average price was. To examine the data directly, first grab the table that contains the result of the query from the DataSet. In the Fill method, ADO.NET put the results of the query into a table named AveragePrice. Then to get a reference to that specific table from the DataSet, retrieve that table by name from the Tables collection.

If there was no data returned, you will have a table with no rows in it. If this is the case, then return the average price as 0. If there is one row in the table – a SQL statement to calculate an average will return at most one row – then look at the value contained in the field named AveragePrice and return that value as the average price for the product type. The field named AveragePrice does not exist in the physical database, but is an alias that is created with the SQL SELECT statement to hold the results of the AVG function:

         If (Not priceTable.Rows(0).IsNull("AveragePrice")) Then
Return CDbl(priceTable.Rows(0)("AveragePrice"))
Else
Return 0
End If
End Function
End Class
End Namespace