Visual Basic 2005: A Developer's Notebook/Web Applications
|Visual Basic 2005: A Developer's Notebook|
Of all the changes in .NET, no part of the framework has undergone as many tweaks, tune-ups, and enhancements as ASP.NET, the popular platform for building web applications. Microsoft developers have piled on new features in an aggressive attempt to reduce the amount of code you need to write by at least 75 percent. Remarkably, they may have achieved their goal.
Create a Web Application in Visual Studio 2005
Visual Studio .NET was the first product to equip ASP developers with a professional IDE, along with debugging and syntax-checking tools. However, creating ASP.NET projects still never seemed quite as easy as creating projects for ordinary Windows and console applications. Part of the trouble was managing the interaction between Visual Studio and Internet Information Services (IIS). Happily, Visual Studio 2005 dramatically improves the design-time experience for web applications by providing a new way to work with web projects.
How do I do that?
To create a new web project in Visual Studio 2005 or Visual Studio Web Developer 2005 (Visual Studio 2005 Express isn't up to the task), select File → New → Web Site, not File → New → Project. You'll see the New Web Site dialog box (see Figure 4-1), in which you need to choose the location where the web project will be placed, and its development language. In the future, you'll also be able to open a new project based on a starter kit from this window.
Forget virtual directories and other IIS headaches. Visual Studio 2005 (and Visual Web Developer 2005) includes a built-in web server and supports project-less web sites.
Visual Studio starts you out with a new directory containing two files: default.aspx (the entry point for your web application) and default.aspx.vb (the code-behind file). There are no project (.vbproj) or solution (.sln) files, which keeps the file structure simpler and makes it easier to deploy just what you need. Instead, Visual Studio automatically shows all the files and subdirectories in the web application directory.
When you create a new ASP. NET project, Visual Studio creates the directory and saves the default web page immediately. This is different than the behavior you've seen with other project types, where Visual Studio creates a temporary project until you explicitly save it.
As you start working with your web application, you'll find that Visual Studio has a few more surprises in store. One is the ability to automatically create a web.config file when you need it. To try this out, click the Start button to launch your application for the first time. At this point, Visual Studio notices that you don't have a web.config file. It then asks you if you'd like to add one automatically to enable debugging (Figure 4-2).
The web.config file that Visual Studio creates is noticeably cleaner than the automatically generated version in Visual Studio .NET 2003. It only contains the information you need (in this case, the debugging settings). As you make other configuration changes to your application using Visual Studio, additional sections are added automatically. Once again, Visual Studio places the emphasis on simplicity and transparency.
When you run your web pages, Visual Studio's integrated web server starts automatically (look for a small icon in the system tray). As a result, you don't need to use IIS to test a web site. Visual Studio's scaled-down web server provides better security because it only serves requests that originate from the local computer. It also shuts down once you exit Visual Studio. Best of all, it allows you to create your web pages and web services where you want them, without worrying about creating the right virtual directory first.
You can try out all the labs in this chapter using the bare-bones web application you've created in this lab.
...the web page coding model? If you've spent much time programming ASP.NET web pages, you're probably aware that there are different ways to separate source code from visual content. In previous versions of Visual Studio, code-behind was the only standard that was supported (which conflicted with the default and exclusive setting of Web Matrix, another Microsoft web application IDE, which used inline code). The happy news is that Visual Studio 2005 supports both of these code models.
By default, when you add new web pages, Visual Studio 2005 uses a slightly simpler form of code-behind. Instead of using inheritance, this updated code-behind relies on another new feature called partial classes (see the lab "Split a Class into Multiple Files" in Chapter 2 for more information). Because partial classes provide the ability to merge separate files into one class, the code-behind file you use to handle web page events doesn't need boilerplate initialization code and web control declarations. Instead, it only contains your code, which makes it quite a bit shorter.
You can also use a code-beside model, which stores the .aspx tags and the code in the same file. To insert a new page that uses this model, select Web Site → Add New Item, select Web Form, and uncheck the "Place code in separate file" checkbox. Then, click Add to insert the file. The new file uses the code-beside approach but can be designed and coded just as easily as a code-behind page.
To support these two models, Visual Studio needed to change its compilation model for ASP.NET files. Now, web pages and web services aren't compiled until you access them for the first time. (In previous versions of Visual Studio, the entire web site is compiled each time you launch the application by clicking the Start button.) However, you can still precompile your application after you deploy it in a production environment to ensure the best performance for the first set of requests. To do so, just execute a request for the precompile.axd extension in the root virtual directory once you deploy your application. For example, if your web application is stored in the virtual directory named WebApplication, you would use this URL from the web server computer:
Don't be distracted by the fact that there is no file named precompile.axd in your virtual directory. All .axd requests invoke ASP. NET extensions that are configured in the machine.config configuration file.
Where can I learn more?
ASP.NET gives you still more compilation and deployment options, which you can learn more about from the whitepaper at http://msdn.microsoft.com/library/en-us/dnvs05/html/codecompilation.asp.
Administer a Web Application
Many settings that control the behavior of an ASP.NET application are found in its web.config file, a special XML document that's placed in the virtual directory of a web application. In the past, ASP.NET developers were forced to edit the web.config settings by hand. But your life is about to get simpler thanks to a new ASP.NET 2.0 graphical interface called the Web Site Administration Tool (WAT).
Thanks to the new Web Site Administration Tool, there's no need to edit the web.config configuration file by hand.
How do I do that?
The Web Site Administration Tool (WAT) is installed on your computer with the .NET Framework 2.0. It allows you to configure ASP.NET web application settings using a dedicated web page.
To run the WAT to configure the current web project in Visual Studio, select Website → ASP.NET Configuration. Internet Explorer will automatically log you on under the current user account. Try it. Figure 4-3 shows you the screen you'll see.
To try out WAT, click the Application tab and then click the "Create application settings" link. A pair of text boxes will appear that allow you to define the name and value of a new setting (see Figure 4-4). Enter "AppName" and "My Test ASP.NET Application" respectively, and click Save.
Now, open the web.config file to see the result. You'll find a new <appSettings> section with the following setting defined:
<appSettings> <add key="AppName" value="My Test ASP.NET Application" /> </appSettings>
This illustrates the basic way that WAT works—you interact with a web page, and it generates the settings you need behind the scenes. To edit or remove this setting, you simply need to return to the WAT and select the "Manage application settings" link.
If you want, you can complete this example by writing a simple routine to display the application setting in your page. Just add a label control to your web page and insert the following code in the Page_Load( ) event handler:
Label1.Text = "You are running " & _ ConfigurationSettings.AppSettings("AppName")
Of course, using the WAT to generate application settings is only the beginning. You can also use the WAT to perform the following tasks:
- Use this tab to set the authentication mode, define authorization rules, and manage users. You'll learn about this tab in the upcoming lab "Easily Authenticate Users."
- Use this tab to set application settings (as demonstrated in this lab) and configure web site counters and debugging.
- Use this tab to configure where user role and personalization information is stored. By default, ASP.NET uses Access to store this information in the AspNetDB.mdb in the App_Data subdirectory (in Beta 1) or in a SQL Server database (in Beta 2).
...making changes to the configuration settings of a web application programmatically? Impressively, ASP.NET includes an extensive set of classes for exactly this purpose in the System.Web.Configuration and System.Web.Administration namespaces. You can use these classes to retrieve or alter web application settings in your web page or web service code. In fact, the entire Web Site Administration Tool is written as an ASP.NET application, and you'll find the source code (in C#) in the following directory:
c:\[Windows Directory]\Microsoft.NET\Framework\[Version]\ ASP.NETWebAdminFiles
Where can I learn more?
To learn more about the WAT, look for the index entry "Web Site Administration Tool" in the MSDN Help.
Bind to Data Without Writing Code
Most serious web applications need to retrieve records from a database. In .NET, the database access framework of choice is ADO.NET. However, writing the code to perform common database operations with ADO.NET, such as opening a connection and fetching a table of results, can be tedious and repetitive, which is not what VB programmers have come to expect. To simplify such tasks, ASP.NET 2.0 introduces several new data source controls that greatly simplify the task of retrieving data and binding it to a web page.
With the new ASP.NET data provider controls, you can generate and bind all your database code at design time, without writing a single line of code.
How do I do that?
To use a new ASP.NET 2.0 data source control, all you need to do is to drag it from the Visual Studio toolbox to a web page, configure a few of its properties, and then bind it to other controls that display the data it exposes. When you run the web page, the data source control performs the heavy lifting, contacting your database and extracting the rows you need.
ASP.NET ships with several data source controls, and more are planned. Although the list has changed from build to build, the latest release includes:
- Interacts with a SQL Server database (Version 7.0 or later).
- Interacts with XML data from a file or some other data source.
- Interacts with a custom object that you create. The next lab, "Bind Web Controls to a Custom Class," provides more details about this technique.
To try out no-code data binding, drag a new SqlDataSource onto the design surface of a web page from the Data section of the toolbox. Then, click the control's smart tag and choose Configure Data Source. Visual Studio will walk you through a short wizard in which you specify the connection string for your database (which is then set in the ConnectionString property) and the query you want to perform (which is then set in the SelectCommand property). Find your database server, and select the Northwind database. Although you can build the query dynamically by selecting the columns in a table, for this example just specify the SQL string "SELECT ContactName FROM Customers".
Other data sources are planned to allow easy retrieval of everything from directory listings to web service data.
When you're finished, Visual Studio will have added a SqlDataSource control tag to your web page, which looks something like this:
<asp:SqlDataSource ID="SqlDataSource1" Runat="server" SelectCommand="SELECT ContactName FROM Customers" ConnectionString= "Data Source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=Northwind"> </asp:SqlDataSource>
This data source defines a connection to the Northwind database, and a Select operation that retrieves a list of all contact names in the Customers table.
Remember, to modify any ASP. NET control, you have two choices. You can select it and make changes in the Properties window, or you can switch to Source view and edit the control tag.
Binding a control to your data source is easy. To try this out, drag a BulletedList control onto your web page, which you can use to show the list of contact names from the Customers table. Click the smart tag, and select Connect to Data Source. You can then choose the data source you want to use (which is the data source created in the last step) and the field you want to display (which is ContactName).
Here's the finished tag:
<asp:BulletedList ID="BulletedList1" Runat="server" DataSourceID="SqlDataSource1" DataTextField="ContactName"> </asp:BulletedList>
Amazingly enough, these two control declarations are all you need to create this data-bound page. When you run the page, the BulletedList will request data from the SqlDataSource, which will fetch it from the database using the query you've defined. You don't need to write a line of code.
For a little more sophistication, you could use another control to filter the list of contacts by some other piece of criteria, like country of residence. This raises a new problem—namely, how can you update the query in the SqlDataSource.SelectCommand according to the value entered in the other control?
ASP.NET solves this problem neatly with parameters. To try it out, start by adding a new data source that fetches a list of customer countries from the database. Here's an example that works in this case:
<asp:SqlDataSource ID="Countries" Runat="server" ConnectionString="..." SelectCommand="SELECT DISTINCT Country FROM Customers"> </asp:SqlDataSource>
Next, add a DropDownList control named lstCountries to expose the country list. You can use the same approach as when you wired up the BulletedList, or you can type the tag in by hand. Here's the completed tag you need:
<asp:DropDownList ID="lstCountries" Runat="server" DataValueField="Country" DataTextField="Country" DataSourceID="Countries" AutoPostBack="True"> </asp:DropDownList>
Now you can modify the query that creates the customer list. First, you insert a named parameter into your query. Remember to place an @ symbol at the beginning of the parameter name so SqlDataSource can recognize it. In this example, use @Country. (The @ denotes a named parameter when using the SQL Server provider.)
Here's what the revised data source tag should look like:
<asp:SqlDataSource ID="SqlDataSource1" Runat="server" SelectCommand="SELECT ContactName FROM Customers WHERE Country='@Country' ... </asp:SqlDataSource>
Next, you add a definition that links the parameter to the appropriate control. Once again, you can configure this information in Visual Studio or by hand. In Visual Studio, select the SqlDataSource control and click the ellipses next to the SelectQuery property in the Properties window. (Truthfully, there is no real SelectQuery. That's just the way Visual Studio exposes the SelectCommand and SelectParameters properties to make it easier to edit them as a single unit at design time.) In this case, you need to create a new control parameter that retrieves the SelectedValue property of the lstCountries control.
Here's the revised data source tag once you've added the parameter definition:
<asp:SqlDataSource ID="SqlDataSource1" Runat="server" SelectCommand="SELECT ContactName FROM Customers WHERE Country=@Country" ConnectionString= "Data Source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=Northwind" <SelectParameters> <asp:ControlParameter Name="Country" ControlID="lstCountries" PropertyName="SelectedValue"> </asp:ControlParameter> </SelectParameters> </asp:SqlDataSource>
Note that the name of the control parameter matches the name of the parameter in the SQL expression, with one minor quirk: the leading @ symbol is always left out.
Figure 4-5 shows the completed page. When you select a country from the drop-down list, the bulleted customer list is refreshed with the matching customers automatically. You now have a fair bit of functionality, and still have not written any code.
This example should already suggest possibilities where you can use multiple data source controls. For example, imagine you want to provide a master-detail view of orders by customer. You could use one data source to fill a listbox with customers. When the user selects a customer, you could then use your other data source to perform a query for the linked orders and show it in a different control.
...reasons not to use the new code-free data-binding controls? Many right-thinking developers steer clear of data-binding techniques because they embed database details into the user-interface code. In fact, that's exactly what this example does, which has negative consequences for maintainability, optimization, and debugging. Quite simply, with database details strewn everywhere in a large site, it's hard to stay consistent.
ASP.NET developers haven't forgotten about this side of things. With a little care, you can use the data source providers and still centralize your database logic. One of the best ways to do so is to use the ObjectDataSource control, which allows you to link to a custom class that you've created with data access code. The next lab, "Bind Web Controls to a Custom Class," demonstrates this technique.
Data sources also provide a useful place to add more advanced functionality. One of the most interesting examples is caching. If you set EnableCaching to True, the data source control will automatically insert the retrieved data into the ASP.NET cache and reuse it in future requests, potentially reducing your database load dramatically. You can configure the amount of time an item is cached by setting the CacheDuration and CacheExpirationPolicy properties.
Where can I learn more?
For more on caching and other advanced scenarios, look up the index entry "data source controls" in the MSDN help library.
Bind Web Controls to a Custom Class
Well- designed applications rigorously separate their data access logic from the rest of their code. In ASP.NET 2.0, you can achieve this separation while still using the new ASP.NET data source controls for convenient no-code-required design-time data binding. The secret is to use the new ObjectDataSource control, which knows how to fetch results from a data access class. You can then bind other controls to the ObjectDataSource for quick and easy web page display.
Want to use data source binding without scattering database details throughout dozens of web pages? The ObjectDataSource control provides the solution.
How do I do that?
To use the ObjectDataSource control, you must first create a custom class that retrieves the data from the database. The database class will contain one method for every database operation you want to perform. Methods that retrieve results from the database can return DataTable or DataSet objects, collections, or custom classes.
Example 4-1 shows a database class called CustomerDB that provides a single GetCustomers( ) method. The GetCustomers( ) method queries the database and returns a collection of CustomerDetails objects. The CustomerDetails object is also a custom object. It simply wraps all the details of a customer record from the database.
Example 4-1. A custom database class
Imports System.Data.SqlClient Imports System.Collections.Generic Public Class CustomerDB Private ConnectionString As String = _ "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI" Public Function GetCustomers( ) As List(Of CustomerDetails) Dim Sql As String = "SELECT * FROM Customers" Dim con As New SqlConnection(ConnectionString) Dim cmd As New SqlCommand(Sql, con) Dim Reader As SqlDataReader Dim Customers As New List(Of CustomerDetails) Try con.Open( ) Reader = cmd.ExecuteReader( ) Do While Reader.Read( ) Dim Customer As New CustomerDetails( ) Customer.ID = Reader("CustomerID") Customer.Name = Reader("ContactName") Customers.Add(Customer) Loop Catch Err As Exception Throw New ApplicationException( _ "Exception encountered when executing command.", Err) Finally con.Close( ) End Try Return Customers End Function End Class Public Class CustomerDetails Private _ID As String Private _Name As String Public Property ID( ) As String Get Return _ID End Get Set(ByVal Value As String) _ID = Value End Set End Property Public Property Name( ) As String Get Return _Name End Get Set(ByVal Value As String) _Name = Value End Set End Property End Class
There are a couple of important points to note about this example. First, the database class must be stateless to work correctly. If you need any information, retrieve it from the custom application settings in the web.config file. Second, notice how the CustomerDetails class uses property procedures instead of public member variables. If you use public member variables, the ObjectDataSource won't be able to extract the information from the class and bind to it.
To use the custom data access class in a data-binding scenario, you first need to make it a part of your web application. You have two options:
- Place it in a separate class library project and then compile it to a DLL file. Then, in the web application, add a reference to this assembly. Visual Studio will copy the DLL file into the Bin subdirectory of your web application.
- Put the source code in an ordinary .vb file in the App_Code subdirectory of your web application. ASP.NET automatically compiles any source code that's in this directory and makes it available to your web application. (To make sure it's compiled, choose Build → Build Website before going any further.)
Once you've taken one of these steps, drag an ObjectDataSource from the data tab of the Visual Studio toolbox onto the design surface of a web page. Click the control's smart tag and choose Configure Data Source. A wizard will appear that lets you choose your class from a drop-down list (a step that sets the TypeName property) and asks which method you want to call when performing a query (which sets the MethodName property).
Here's what the completed ObjectDataSource control tag looks like in the .aspx page of this example:
<asp:ObjectDataSource ID="ObjectDataSource1" Runat="server" TypeName="CustomerDB" SelectMethod="GetCustomers"> </asp:ObjectDataSource>
You are now able to bind other controls to the properties of the CustomerDetails class. For example, this BulletedList exposes the CustomerDetails.Name information for each object in the collection:
<asp:BulletedList ID="BulletedList1" Runat="server" DataTextField="Name" DataSourceID="ObjectDataSource1"> </asp:BulletedList>
When you run the application, the BulletedList requests data from the ObjectDataSource. The ObjectDataSource creates an instance of the CustomerDB class, calls GetCustomers( ), and returns the data.
...updating a database through an ObjectDataSource? Not a problem. Both the ObjectDataSource and the SqlDataSource controls discussed in the previous lab, "Bind to Data Without Writing Code" support inserting, updating, and deleting records. With SqlDataSource, you simply need to set properties such as DeleteCommand, InsertCommand, and UpdateCommand with the appropriate SQL. With the ObjectDataSource, you set properties such as DeleteMethod, InsertMethod, and UpdateMethod by specifying the corresponding method names in your custom data access class. In many cases, you'll also need to specify additional information using parameters, which might map to other controls, query string arguments, or session information. For example, you might want to delete the currently selected record, or update a record based on values in a set of text boxes. To accomplish this, you need to add parameters, as described in the previous lab "Bind to Data Without Writing Code."
Once you've configured these operations (either by hand or by using the convenient design-time wizards), you can trigger them by calling the Delete( ), Insert( ), and Update( ) methods. Other controls that plug in to the data source control framework can also make use of these methods. For example, if you configure a SqlDataSource object with the information it needs to update records, you can enable GridView editing without needing to add a line of code. You'll see an example of this technique with the DetailsView control in the upcoming lab "Display Records One at a Time."
Where can I learn more?
For more information, look up the index entry "data source controls" in the MSDN help library. To learn about the new GridView, refer to the next lab, "Display Interactive Tables Without Writing Code."
Display Interactive Tables Without Writing Code
The ASP.NET 1.0 and 1.1 DataGrid control was tremendously popular, but implementing some of its most desirable features often required writing a lot of boilerplate code. For example, if you wanted to let users page through rows of data, it was up you to query the database after every postback, retrieve the requested page, and set the range of rows that you wanted to display. With the new GridView control, these headaches are a thing of the past.
The new GridView control lets you create and display tables of data that users can sort, page through, and edit without requiring you to write a single line of code.
In preparing for ASP.NET 2.0, Microsoft architects chose not to release a new version of the current DataGrid in order to simplify backward compatibility. Instead, the new GridView control duplicates and extends the functionality of the DataGrid, while making its features available to developers through a much simpler programming model.
How do I do that?
To use the new GridView control, drag it from the Data section of the Visual Studio toolbox onto the design surface of a web page. For hassle-free data binding, you can add a SqlDataSource control (described in the lab "Bind to Data Without Writing Code") or use an ObjectDataSource control in conjunction with a custom data access object, as explained in "Bind Web Controls to a Custom Class." In this case, we'll use a SqlDataSource control and the select query shown here to retrieve all fields and records in the Customers table of the Northwind database. Here's the final data source tag:
<asp:SqlDataSource ID="CusomtersList" Runat="server" SelectCommand="SELECT * FROM Customers" ConnectionString= "Data Source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=Northwind"> </asp:SqlDataSource>
Now, set the GridView.DataSourceID property to the name of the SqlDataSource (in this example, CustomersList). This binds the GridView to the SqlDataSource.
At this point, you can run your page and see a simple HTML table with a full list of customers. However, to make your table look respectable, there are a number of additional steps you'll want to take. These include:
You should be able to see the columns of your grid at design time. If you don't, choose Refresh Schema on the SqlDataSource smart tag (to get the column information from the database) and then choose Refresh Schema on the GridView smart tag.
- Setting the Font property to use a more attractive font. A common choice that's supported by most web browsers is Verdana (use a size of X-Small or XX-Small).
- Applying some formatting with styles. You can set colors, fonts, and sizes for the FooterStyle, HeaderStyle, RowStyle, and more using the Properties window. Or, to change the complete look in a hurry, click the GridView smart tag and choose AutoFormat. When you choose one these presets, all the GridView styles are set automatically.
Making the GridView look respectable is only part of the work. You can also switch on various GridView features using options in the GridView smart tag. Here are some links you can click to get quick results:
- Enable Paging
- This option sets the AllowPaging property to True. The GridView will then split long lists of records into separate pages (each with the number of rows designated in the PageSize property). Users can move from page to page by clicking numbered links that appear at the bottom of the GridView.
- Enable Sorting
- This option sets AllSorting to True. The GridView will then provide column hyperlinks. When the user clicks one, the whole table will be resorted in alphabetic order (or ascending numeric order) according to that column.
- Enable Selection
- This option adds a Select link in a new column at the left side of the grid. Users can click this link to select the row (at which point the SelectedIndex property will be set accordingly).
- Enable Deleting
- This option adds a Delete link in a new column at the left side of the grid. Users can click this link to delete the row from the database. You'll only see this option if you've defined a DeleteCommand for the attached data source.
- Enable Editing
- This option adds an Edit link in a new column at the left side of the grid. Users can click this link to put a row in edit mode (at which point an Update and Cancel link will appear, allowing them to push the change to the database or roll it back). You'll only see this option if you've defined an UpdateCommand for the attached data source.
Figure 4-6 shows a table that supports paging and sorting by column, which was generated by GridView without using a single line of custom code.
...fine-tuning the GridView display? For example, you might want to tweak the sort order, the text used for the selection and editing links, the column titles, or the order of columns. You might also need to set default text and format strings. To perform any of these tasks, you simply customize the column objects that the GridView generates based on the format of the data source records. The easiest way to do so is to select Edit Columns link on the GridView smart tag and use the Fields dialog to customize the properties of each column. Try it.
Display Records One at a Time
While the GridView control is a perfect tool for presenting the records of a database as rows of data in a table, it becomes less convenient when you have records with many fields (especially if some fields are quite long), and you want to let users manipulate or add to the data they contain. One solution is to show only selected fields in a grid, but there are times when you need to display an entire record on a page and give the user the ability both to edit individual records and to add new records to the database. In ASP.NET, the handy new DetailsView gives you all the functionality you need to deal with individual records for free (i.e., without having to write your own code).
The new DetailsView control gives you a convenient way to let users view, edit, insert, and delete individual records.
How do I do that?
The new DetailsView control works in much the same way as the GridView control described in the previous lab, "Display Interactive Tables Without Writing Code." The difference is that the DetailsView displays a single record at a time. By default, all the fields are displayed in a table, each field in a row of its own, listing from top to bottom.
To add a DetailsView to a web page, simply drag it onto the design surface from the Visual Studio Toolbox Data tab. Next, click its smart tag and select Configure Data Source to attach it to a data source control. You can also use the Auto Forms link in the smart tag to apply a rich set of styles to the grid it displays.
Because the DetailsView can only show a single record, you need to take extra steps to make sure it shows the right one. To do this, you need to use a filter expression (a SQL expression that limits the records you see according to the criteria you specify). You add the filter expression to the data source by setting the FilterExpression and FilterParameters properties of the DetailsView.
For example, consider the page that is shown in Figure 4-7. GridAndDetails.aspx contains both a GridView showing select information about the first five records and a DetailsView showing all fields of the selected record.
This page needs two data sources, one for the GridView (which is defined in the same way as described in the lab "Display Interactive Tables Without Writing Code.") and one for the DetailsView. The DetailsView data source definition looks like this:
<asp:SqlDataSource ID="SingleCustomerSource" Runat="server" SelectCommand="SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Country FROM Customers WHERE CustomerID=@CustomerID" ConnectionString= "Data Source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=Northwind" > <SelectParameters> <asp:ControlParameter Name="CustomerID" ControlID="GridView1" PropertyName="SelectedValue"> </asp:ControlParameter> </SelectParameters> </asp:SqlDataSource>
This SELECT query selects only the single row that matches the CustomerID that's selected in the GridView control.
It's easy to hook up a basic DetailsView like the one shown in Figure 4-7. But life becomes even better if you do the work to add editing, deleting, and inserting abilities to the DetailsView. You can add all of these frills with the click of a button, provided you first make sure the connected data source has all the information it needs. For example, if you want to create a SqlDataSource that supports deleting, you need to configure the DeleteCommand and DeleteParameters properties. To create a data source that supports inserting new records, you need to add an InsertCommand and InsertParameters.
Adding these extra details is surprisingly easy. All you need to do is understand a few rules:
- All updates are performed through parameterized commands that use named placeholders instead of values.
- The parameter name is the same as the field name, with a preceding @ symbol. For example, the ContactName field becomes the @ContactName parameter.
- When you write the Where clause for your query, you need to precede the parameter name with the text original_. This indicates that you want to use the original value (which ignores any changes the user may have made). For example, @CustomerID becomes @original_CustomerID.
If you follow these rules, the DetailsView control will hook up the parameter values automatically. To try this out, follow these steps.
First, write a parameterized command that uses named placeholders instead of values. For example, here's a parameterized DeleteCommand for deleting the currently selected record, which follows the list of rules above:
DELETE Customers WHERE CustomerID=@original_CustomerID
This command deletes the currently selected record. The amazing thing about this command is that because it follows the naming rules listed above, you don't have to worry about supplying a value. Instead, you simply define the parameter as shown below, and the DetailsView will use the CustomerID from the currently displayed record:
<asp:SqlDataSource ID="SingleCustomerSource" Runat="server" DeleteCommand="DELETE Customers WHERE CustomerID=@original_CustomerID" ... > <DeleteParameters> <asp:Parameter Name="CustomerID"> </asp:Parameter> </DeleteParameters> ... </asp:SqlDataSource>
Example 4-2 shows a completed SqlDataSource that defines commands for update, insert, and delete operations in this way.
Example 4-2. A SqlDataSource tag
<asp:SqlDataSource ID="SingleCustomerSource" Runat="server" ConnectionString= "Data Source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=Northwind" SelectCommand= "SELECT CustomerID,CompanyName,ContactName,ContactTitle,Address, City,Country FROM Customers" FilterExpression="CustomerID='@CustomerID'" DeleteCommand="DELETE Customers WHERE CustomerID=@original_CustomerID" InsertCommand= "INSERT INTO Customers (CustomerID,CompanyName,ContactName,ContactTitle,Address, City,Country) VALUES (@CustomerID,@CompanyName,@ContactName,@ContactTile,@Address, @City,@Country)" UpdateCommand= "UPDATE Customers SET CompanyName=@CompanyName,ContactName=@ContactName, ContactTitle=@ContactTitle,Address=@Address,City=@City,Country=@Country WHERE CustomerID=@original_CustomerID"> <FilterParameters> <asp:ControlParameter Name="CustomerID" Type="String" ControlID="GridView1" PropertyName="SelectedValue"> </asp:ControlParameter> </FilterParameters> <DeleteParameters> <asp:Parameter Name="CustomerID"> </asp:Parameter> </DeleteParameters> <InsertParameters> <asp:Parameter Name="CustomerID"></asp:Parameter> <asp:Parameter Name="CompanyName"></asp:Parameter> <asp:Parameter Name="ContactName"></asp:Parameter> <asp:Parameter Name="ContactTile"></asp:Parameter> <asp:Parameter Name="Address"></asp:Parameter> <asp:Parameter Name="City"></asp:Parameter> <asp:Parameter Name="Country"></asp:Parameter> </InsertParameters> <UpdateParameters> <asp:Parameter Name="CompanyName"></asp:Parameter> <asp:Parameter Name="ContactName"></asp:Parameter> <asp:Parameter Name="ContactTitle"></asp:Parameter> <asp:Parameter Name="Address"></asp:Parameter> <asp:Parameter Name="City"></asp:Parameter> <asp:Parameter Name="Country"></asp:Parameter> <asp:Parameter Name="CustomerID"></asp:Parameter> </UpdateParameters> </asp:SqlDataSource>
This tag is a long one, but the parameter definitions are surprisingly simple. Even better, Visual Studio wizards can help you build insert, update, and delete commands quickly. Just click the ellipsis next to the property name in the Properties window (e.g., the DeleteCommand property), and then type in the parameterized command and click Refresh Parameters. Refreshing automatically generates all the parameter tags based on your command.
To configure the DetailsView so that it uses these commands, just click the smart tag and add a checkmark next to the options Enable Inserting, Enable Deleting, and Enable Updating. This sets Boolean properties like AutoGenerateInsertButton, AutoGenerateDeleteButton, and AutoGenerateEditButton.
Figure 4-8 shows a DetailsView in edit mode.
...updating the GridView so it stays synchronized with the DetailsView? If you don't take any extra steps you'll notice a little inconsistency; changes you make editing, inserting, or deleting records with the DetailsView won't appear in the GridView until you manually refresh the page. To get around this problem, you need to add a little event-handling code. In this case, the important DetailsView events are ItemInserted, ItemDeleted, and ItemUpdated, which fire after each of these edit operations has completed. Here's code you can add to each event handler to refresh the grid when an item is inserted, deleted, or updated:
Sub DetailsView1_ItemUpdated(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.DetailsViewUpdatedEventArgs) GridView1.DataBind( ) End Sub
The DetailsView has much more functionality that you can harness. For example, you can handle the ItemInserting, ItemDeleting, and ItemUpdating events to check the requested change, perform data validation, and stop the update from being committed. You can also create your own edit controls using templates. For more information about these techniques, look up the index entry "DetailsView control" in the MSDN Help.
Achieve a Consistent Look and Feel with Master Pages
Most professional web sites standardize their layout. On the O'Reilly web site (http://www.oreilly.com), for example, a navigation bar always appears on the left-hand side of a content page, and a company logo is displayed at the top. These details remain consistent as the user moves from page to page.
Need to enforce a regular design across all the pages in a web site? ASP. NET 2.0 has a new master pages feature that allows you to create page templates.
In ASP.NET 1.0 and 1.1, you can create web sites with standardized layouts, but there aren't any tools to make it easy. For example, with user controls you can reuse blocks of user interface, but there isn't any way to ensure that they always end up in the same position on different pages. Using HTML frames, you can break up a web browser window so it shows multiple web pages, but it's extremely difficult to keep all the web pages properly coordinated. In ASP.NET 2.0, these imperfect solutions are replaced with a new feature called master pages, a page templating system.
How do I do that?
To create a basic master page in Visual Studio, select Website → Add New Item from the menu, select Master Page, and click OK to add the item.
Master pages are similar to ordinary ASP.NET pages in the sense that they can contain HTML, web controls, and code. However, they have a different extension ( .master instead of .aspx), and they can't be requested directly by a browser. Instead, other pages (known as content pages) can use the master page.
You design the master page as you would a normal ASP.NET web page, adding the text and controls you need to get a consistent look across all pages of your site. The elements you add to the master page cannot be modified by the content pages that make use of it. You use the new ContentPlaceHolder control to mark off areas reserved for content that will vary from page to page. In these regions of the master page, content pages can add their own controls and HTML.
Consider the sample master page whose source is shown in Example 4-3. It creates two tables. The topmost table holds the header region, and the second table contains the rest of the page. The second table is split into two cells, a cell on the left for a navigation bar, and a cell on the right that contains a ContentPlaceHolder tag. Any content page that uses (i.e., inherits from) this master page can completely control the content of that cell, but not of any other cell in that table or other tables on the master page.
Example 4-3. A master page that uses a table
<%@ Master language="VB" %> <html> <head id="Head1" runat="server"> <title>Master Page</title> </head> <body> <form id="Form1" runat="server"> <table id="header" width="100%" height="80px" cellspacing="1" cellpadding="1" border="1"> <tr> <td width="100%" style="TEXT-ALIGN: center"> This is the Master Page fixed header. </td> </tr> </table> <table id="main" width="100%" height="100%" cellspacing="1" cellpadding="1" border="1"> <tr> <td valign=top width="100px"> Put the site map here (on left). </td> <td valign=top > <asp:ContentPlaceHolder id="content" runat="Server"> Put your content here. </asp:ContentPlaceHolder> </td> </tr> </table> </form> </body> </html>
Figure 4-9 shows the master page at design time. For more advanced layout, you could use nested tables, or put the ContentPlaceHolder tag inside a single cell of a more complex table, which includes multiple columns and rows.
To create a new content page, right-click the Solution Explorer and select Add New Item. Choose the Web Form option, give the file a name, and then select the "Select master page" checkbox. When you click Add, a dialog box will appear, prompting you to select one of the master pages in the current web application. Select the master page in Example 4-3, and click OK.
When you create a content page, it automatically gets the same look as the master page from which it derives. You can add content only inside the content areas designated by a ContentPlaceHolder control. The predefined header and sitemap regions of the master page will appear grayed out in Visual Studio.
The actual markup for content pages looks a little different than ordinary pages. First of all, the Page directive links to the master page you're using, as shown here:
<%@ Page MasterPageFile="Site.master" %>
In order to add content to the page, you need to enter it inside a special Content tag. The Content tag links to one of the ContentPlaceHolder tags you created in the master page. For example, if you want to add content to the master page example shown earlier, you need a Content tag that looks like this:
<asp:Content ContentPlaceHolderID="content" Runat="server"> ... </asp:Content>
This ContentPlaceHolderID attribute must match the id attribute of one of the ContentPlaceHolder tags in the master page. Note that you do not need to add Content tags to the content page in the same order as the ContentPlaceHolder tags appear in the master page. Visual Studio will create the content tag automatically as you add controls to the content page.
Example 4-4 shows the code you need to implement a very simple content page based on the master page shown in Example 4-3. Note that the page doesn't include tags like <html>, <header>, <body>, and <form>, because these tags are only defined once for a page, and they're already included in the master page.
You don't need to specify content for each placeholder. If you don't, ASP. NET shows whatever content is in the ContentPlaceHolder tag on the master page (if any).
Example 4-4. A content page with a picture and text
<%@ page language="VB" MasterPageFile="Site.master" %> <asp:Content ContentPlaceHolderID=content Runat=server> <asp:Image ID="image1" ImageUrl="oreilly_header.gif" Runat="server" /> <br /> <br /> <i>This is page-specific content!</i> <hr /> </asp:Content>
Figure 4-10 shows the resulting content page.
You can create master pages that use other previously defined master pages, effectively nesting one master page inside another. Such a nested design might make sense if you need to define some content that appears on every page in a web site (like a company header) and some content that appears on many but not all pages (like a navigation bar).
One good reason to use master pages is to dedicate some web page real estate for some sort of navigation controls. The next lab, "Add Navigation to Your Site," explores this topic in more detail.
...other ways to help ensure consistency? ASP.NET 2.0 introduces another feature for standardizing web sites called control theming. While master pages ensure a regular layout and allow you to repeat certain elements over an entire site, theming helps to make sure web page controls have the same "look and feel." Essentially, a control theme is a set of style attributes (such as fonts and colors) that can be applied to different controls.
Where can I learn more?
For more information, look for the index entry "themes" in the MSDN Help.
Add Navigation to Your Site
Most web sites include some type of navigation bar that lets users move from one page to another. In ASP.NET 1.0 and 1.1, it's easy enough to create these navigation controls, but you need to do so by hand. In ASP.NET 2.0, a new sitemap feature offers a much more convenient pre-built solution. The basic principle is that you define the structure of your web site in a special XML file. Once you've taken that step, you can configure a list or tree control to use the sitemap data—giving you a clickable navigation control with no code required.
ASP. NET 2.0 provides new navigation features that let you create a sitemap and bind it to different controls.
How do I do that?
The first step in using ASP.NET's new sitemap feature is to define the structure of your web site in an XML file named web.sitemap. To add this file to your site in Visual Studio, right-click the Solution Explorer and select Add New Item. Select the Site Map file type and click Add.
The first ingredient you need in the web.sitemap file is the root <siteMap> tag:
In the <siteMap> tag, you add one <siteMapNode> child element for each entry you want to show in the sitemap. You can then give a title, description, and URL link for each entry using attributes. Here's an example:
<siteMapNode title="Home" description="Home Page" url="default.aspx" />
Notice that this tag ends with the characters /> instead of just >. This indicates that it's an empty element—in other words, it doesn't contain any other elements. However, if you want to build a multi-level sitemap, you have to nest one <siteMapNode> element inside another. Here's an example:
<siteMapNode title="Home" description="Home Page" url="default.aspx" > <siteMapNode title="Products" description="Order Products" url="produ.aspx" /> </siteMapNode>
Example 4-5 shows a sitemap with six links in three levels.
Example 4-5. A multi-level sitemap
<?xml version="1.0" ?> <siteMap> <siteMapNode title="Home" description="Home" url="default.aspx"> <siteMapNode title="Personal" description="Personal Services" url="personal.aspx"> <siteMapNode title="Resume" description="Download Resume" url="resume.aspx" /> </siteMapNode> <siteMapNode title="Business" description="Business Services" url="business.aspx"> <siteMapNode title="Products" description="Order Products" url="products.aspx" /> <siteMapNode title="Contact Us" description="Contact Information" url="contact.aspx" /> </siteMapNode> </siteMapNode> </siteMap>
Once you create a sitemap, it's easy to use it on a web page, thanks to the new SiteMapDataSource control. This control works much like the other data source controls discussed in "Bind to Data Without Writing Code." However, it doesn't require any properties at all. Once you add the SiteMapDataSource, ASP.NET automatically reads the web.sitemap file and makes its data available to your other controls:
<asp:SiteMapDataSource ID="SiteMapDataSource1" Runat="server" />
Now you can bind just about any other control to the SiteMapDataSource. Because sitemaps are, by default, hierarchical, they work particularly well with the new TreeView control. Here's a TreeView control that binds to the sitemap data:
<asp:TreeView ID="TreeView1" Runat="server" DataSourceID="SiteMapDataSource1" Font-Names="Verdana" Font-Size="8pt" ForeColor="Black" ImageSet="BulletedList" Width="149px" Height="132px"> </asp:TreeView>
The resulting TreeView doesn't just show the sitemap, it also renders each node as a hyperlink that, if clicked, sends the user to the appropriate page. Figure 4-11 shows a content page that's based on a master page that uses a TreeView with a sitemap. (Refer to the lab "Achieve a Consistent Look and Feel with Master Pages" for more information about master pages.)
...customizing the sitemap? There's a lot more you can do to control the look of a site as well as its behavior. Here are some starting points:
- Show a sitemap in a non-hierarchical control
- Controls like the ListBox and GridView don't support the sitemap's tree-based view. To solve this problem, set the SiteMapDataSource.SiteMapViewType property to Flat instead of Tree so that the multi-layered sitemap is flattened into a single-level list. You can also use the Flat option with a TreeView to save screen real estate (because subsequent levels won't be indented).
- Vary the sitemap displayed in different pages
- To accomplish this, put all the sitemap information you need into the same web.sitemap file, but in different branches. Then, set the SiteMapDataSource.StartingNodeUrl to the URL of the page you want to use as the root of your sitemap. The SiteMapDataSource will only get the data from that node, and all the nodes it contains.
- Make the sitemap collapsible
- If you have a large sitemap, just set the TreeView.ShowExpandCollapse to True, and the familiar plus boxes will appear next to Home, Personal, and Business, allowing you to show just part of the sitemap at a time.
- Fine-tune the appearance of the TreeView
- It's remarkably easy. In the previous example, the TreeView used the bullet style, which shows different bullet icons next to each item. By setting the TreeView.ImageSet to different values available within the TreeViewImageSet enumeration, you can show square bullets, arrows, folder and file icons, and much more. For even more information about tweaking the TreeView (or using it in other scenarios that don't involve sitemaps), look up the reference for the System.Web.UI.WebControls.TreeView class.
- Retrieve the sitemap information from another location
- Maybe you want to store your sitemap in a different file, a database, or some other data source. Unfortunately, the SiteMapProvider doesn't have the ability to retrieve information from these locations—at least not yet. Instead, you'll need to create your own custom sitemap provider. Refer to the MSDN help under the index entry "site map."
Easily Authenticate Users
In ASP.NET 1.0 and 1.1, developers had a handy tool called forms authentication. Essentially, forms authentication kept track of which users had logged in by using a special cookie. If ASP.NET noticed a user who hadn't logged in trying to access a secured page, it automatically redirected the user to your custom login page.
Tired or writing your own authentication code? ASP. NET 2.0 will take care of the drudgery, maintaining user credentials in a database and validating a user login when you ask for it.
On its own, forms authentication worked well, but it still required some work on your part. For example, you needed to create the login page and write the code that examined the user's login name and password and compared it to values in a custom database. Depending on how your application worked, you may also have needed to write code for adding new users and removing old ones. This code wasn't complicated, but it could be tedious.
ASP.NET 2.0 dramatically reduces your work with its new membership features. Essentially, all you need to do is use the methods of the membership classes to create, delete, and validate user information. ASP.NET automatically maintains a database of user information behind the scenes on your behalf.
How do I do that?
The first step in adding authentication to your site is to choose a membership provider, which determines where the user information will be stored. ASP.NET includes membership providers that allow you to connect to a SQL Server database, and several additional providers are planned.
The membership provider is specified in the web.config configuration file. However, rather than typing this information in by hand, you'll almost certainly use the WAT, described in the lab "Administer a Web Application." Just click the Security tab, and click the "Use the security Setup Wizard" link to walk through a wizard that gives you all the options you need. The first question is the access method—in other words, how your visitors will authenticate themselves. Choose "From the internet" to use forms authentication rather than Windows authentication.
The following step allows you to choose the membership provider. You can choose a single provider to use for all ASP.NET features, or separate providers for different features (such as membership, role-based security, and so on). If you choose to use a SQL Server database, you must also run the aspnet_regsql.exe utility, which will walk you through the steps needed to install the membership database. You'll find the aspnet_regsql.exe utility in the c:\[WinDir]\Microsoft.NETƒramework\[Version] directory.
Next, you'll have the chance to create users and roles. Roles are discussed in a later lab ("Use Role-Based Authentication"), so you don't need to create them yet. You don't need to create a test user, because you'll do that through your web site in the next step.
You can choose the name of your login page by modifying the <authentication> section in the web.config file, as shown here:
<?xml version="1.0"?> <configuration> <system.web> <authentication mode="Forms"> <forms loginUrl="Login.aspx" /> </authentication> <!-- Other settings ommitted. --> </system.web> </configuration>
In this case, the login page for the application is named Login.aspx(which is the default). In this page, you can use the shared methods and properties of the System.Web.Security.Membership class to authenticate your users. However, you don't need to, because ASP.NET includes a set of security-related controls that you can drag and drop into your web pages effortlessly. The security controls include:
- Shows the controls needed for a user to log in, including username and password text boxes, and a Login button. Optionally, you can show an email address text box, and you can configure all the text labels in the control by modifying properties like UserNameLabelText and PasswordLabelText.
- Shows one template out of a group of templates, depending on who is currently logged in. This gives you a way to customize content for different users and roles without using any custom code.
- Provides a mechanism through which users can have a forgotten password mailed to them. This feature is disabled by default and requires some tweaking of web.config settings.
- Displays a Login link if the user isn't currently logged in, or a Logout link if the user is logged in.
- Shows the name of the currently logged-in user in a label.
- Allows the user to step through creating a new user account.
- Allows the user to change his or her current password, by specifying the current and new passwords.
You'll find the security controls in the Login tab of the Visual Studio toolbox. To try them out, create a new page named RegisterUser.aspx. Drop a CreateUserWizard control onto the web page. Now run the page and use the wizard to create a new user with the username testuser and the password test.
By default, the CreateUserWizard control uses two steps (shown in Figure 4-12). The first step allows you to specify all your user information, and the second step simply displays a confirmation message.
If you like, you can dig into the backend database to confirm that your user information was saved (after all, it happened automatically, without requiring any custom code). But a better test is to actually create a restricted area of your web page.
First, add the Login.aspx page. To create this page, just drag a Login control onto the page, and you're finished.
Now, it's time to restrict access to a portion of the web site. Select Website → New Folder to create a subdirectory in your web application directory, and name the new directory Secured. Next, create a new page in this directory named Private.aspx, and add the text "This is a secured page."
Now, run the WAT by selecting Website → ASP.NET Configuration. Choose the Security tab. Using this tab, you can examine the list of users (including the test user you added in the previous step) and modify their information. What you really need to do, however, is click the "Create access rules" link to restrict access to the Secured directory. Select the directory in the list, choose the Deny Permission option, and select Anonymous users, as shown in Figure 4-13. Then, click OK to add this rule.
Now you're ready to test this simple security example. Right-click on the Private.aspx, file and choose "Set As Start Page." Then, run your application. ASP.NET will immediately detect that your request is for a secured page and you haven't authenticated yourself. Because you've configured forms authentication, it redirects you to the Login.aspx page.
Now enter the username testuser and the password test in the login control. ASP.NET will validate you using the membership provider and redirect you to the originally requested Private.aspx page.
In other words, by using the CreateUserWizard and Login controls in conjunction with the WAT, you've created an authentication system that restricts access to a specific portion of your web site—all without a single line of code.
...ways to customize the authentication process? If you need to control how authentication, user creation, and other security tasks work, you'll be happy to find that the security controls are easily extensible. You can add new steps to the CreateUserWizard to collect additional data, respond to events that fire when the user is logged in (or denied access), and even convert the steps to editable templates so that you can fine-tune the user interface, adding new controls or removing existing ones.
If you want to go a step further, you can abandon the security controls altogether, but still create almost no-code solutions using the static methods of the System.Web.Security.Membership class. Here are some of the methods you can call:
- CreateUser( )
- Creates a new user record in the data store with a username, a password, and (optionally) an email address.
- DeleteUser( )
- Removes the user record from the data store that has the indicated username.
- GeneratePassword( )
- Creates a random password of the specified length. You can suggest this to the user as a default password when creating a new user record.
- GetUser( )
- GetUser( ) retrieves a MembershipUser record for a user with the given username. You can then examine the MembershipUser properties to find out details such as the user's email address, when the account was created, when the user last logged in, and so on. If you don't specify a username, the GetUser( ) method retrieves the current user for the page.
- GetUserNameByEmail( )
- If you know a user's email address but you don't know the username, you can use this method to get it.
- UpdateUser( )
- After you've retrieved a MembershipUser object, you can modify its properties and then submit the object to the UpdateUser( ) method, which commits all your changes to the user database.
- ValidateUser( )
- This accepts a username and password, and verifies that it matches the information in the database (in which case it returns True). ASP.NET doesn't actually store the unencrypted password in the database—instead, it uses a hashing algorithm to protect this information.
Using these methods, you can quickly construct basic login and user registration pages without needing to write any database code. All you need to do is create the user interface for the page (in other words, add labels, text boxes, and other controls).
For example, to design a customized login page, just create a page with two text boxes (named txtUser and txtPassword) and a button (named cmdLogin). When the button is clicked, run this code:
Sub cmdLogin_Click(ByVal sender As Object, ByVal e As System.EventArgs) If Membership.ValidateUser(txtUser.Text, txtPassword.Text) Then ' ASP.NET validated the username and password. ' Send the user to page that was originally requested. FormsAuthentication.RedirectFromLoginPage(txtUser.Text, False) Else ' The user's information is incorrect. ' Do nothing (or just display an error message). End If End Sub
Notice how simple the code is for this page. Instead of manually validating the user by connecting to a database, reading a record, and checking the fields, this code simply calls the Membership.ValidateUser( ) method, and ASP.NET takes care of the rest.
Just as easily, you can create a page that generates a new user record with the Membership class:
Sub cmdRegister_Click(ByVal sender As Object, ByVal e As System.EventArgs) Dim Status As MembershipCreateStatus Dim NewUser As MembershipUser = Membership.CreateUser(_ txtUser.Text, txtPassword.Text, txtEmail.Text, Status) ' If the user was created successfully, redirect to the login page. If Status = MembershipCreateStatus.Success Then Response.Redirect("Login.aspx") Else ' Display an error message in a label. lblStatus.Text = "Attempt to create user failed with error " & _ Status.ToString( ) End If End Sub
For more information, look up the index entry "Membership class" in the MSDN Help. You can also refer to the next three labs, which build up the basic membership framework with new features:
- "Determine How Many People Are Currently Using Your Web Site"
- Explains how additional membership features can track who's online.
- "Use Role-Based Authorization"
- Describes how you can enhance your authorization logic by assigning users to specific roles, essentially giving them different sets of privileges. This feature isn't handled by the membership service, but by a complementary role manager service.
- "Store Personalized Information"
- Shows how you can store other types of user-specific information in a data store, instead of just usernames, passwords, and email addresses. This feature isn't handled by the membership service, but by a complementary personalization service.
Determine How Many People Are Currently Using Your Web Site
The Web uses HTTP, a stateless protocol that rarely maintains a connection longer than a few seconds. As a result, even as users are reading through your web pages, they aren't connected directly to your server. However, ASP.NET gives you a way to estimate how many people are using your web site at any given moment using timestamps. This information makes a great addition to a community site (e.g., a web discussion forum), and it can also be useful for diagnostic purposes.
Ever wondered how many people are using your site right now? If you're using ASP. NET's personalization features, it's remarkably easy to get a reasonable estimate.
How do I do that?
Every time a user logs in using a membership provider (described in the lab "Easily Authenticate Users"), ASP.NET records the current time in the data store. When the same user requests a new page, ASP.NET updates the timestamp accordingly. To make a guess at how many people are using your web site, you can count the number of users who have a timestamp within a short window of time. For example, you might consider the number of users who have requested a page in the last 15 minutes.
You can retrieve this information from ASP.NET using the new GetNumberOfUsersOnline() method of the Membership class. You can also configure the time window that will be used by setting the UserIsOnlineTimeWindow property (which reflects a number of minutes). It's set to 15 by default.
Here's a code snippet that counts the online users and displays the count in a label:
Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) lblStatus.Text &= "<br>There are " &_ Membership.GetNumberOfUsersOnline( ) & _ " users online right now. That is an estimate based" &_ " on looking at timestamps that fall in the last " &_ Membership.UserIsOnlineTimeWindow & _ " minutes." End Sub
Keep in mind that this count doesn't include anonymous users.
...getting information about exactly which users are online? Unfortunately, ASP.NET doesn't currently provide any way to determine which users are online. The only alternative is to add your own tracking code. For example, you could store this information in a database or add it to an in-memory object such as the Application collection whenever a user logs in. You would also need to store the login time and discard old entries periodically.
Use Role-Based Authorization
In many web applications, all users are not equal. Some might be allowed to perform a carefully restricted set of actions, while others are given free reign to perform more advanced administrative tasks. ASP.NET 2.0 makes it easier than ASP.NET 1.x to assign permissions to different groups of users using the new role-management service.
Do you need to give different privileges to different types of users? The easiest way to implement this logic is by using ASP. NET's new role-management service.
How do I do that?
ASP.NET uses a role-management service to manage the storage and retrieval of role-based information. ASP.NET gives you the flexibility to use different role-manager providers to store the role information in different data sources. Usually, you'll use the same data store that you use for membership (as described in the lab "Easily Authenticate Users"). Because the membership provider and the role-manager provider use different tables, you don't need to worry about a conflict.
Role management is not enabled by default. You can enable it by using the WAT, as described in the lab "Administer a Web Application." Just select Website → ASP.NET Configuration and choose the Security tab. Then click the "Enable roles" link in the Roles box. This modifies the web.config as needed. However, you'll still need to configure the roles you want to use.
The easiest way to add role information is also through the WAT. To do so, click the "Create or Manage roles" link in the Roles box on the Security page. This presents a page where you can add new roles and assign users to existing roles. To add a new role, type in the role name and click Add Role. You'll see the role appear in the list below. Figure 4-14 shows an example with two groups, Administrators and Sales Officials. Note that you won't see group membership on this page.
To change group membership, click the Manage link next to the appropriate role. Because a typical system could easily have hundreds of users, the WAT does not attempt to show all the users at once. Instead, it allows you to specify the user that you want to add to the role by typing in the name, browsing an alphabetical directory, or using a search with wild cards (as in John* to find usernames starting with John). Once you've found the appropriate user, place a checkmark in "User Is In Role" column to add the user to the role, or clear the checkbox to remove the user, as shown in Figure 4-15.
Using this tab, you can examine the list of users (including the test user you added in the previous step) and modify their information. What you really need to do, however, is click the "Create access rules" link to restrict access to the Secured directory. Select the directory in the list, choose the Deny Permission option, and select Anonymous users, as shown in Figure 4-13. Then, click OK to add this rule.
When a user logs in using forms authentication (as described in the lab "Easily Authenticate Users"), the role-management service automatically retrieves the list of roles that the user belongs to and stores them in an encrypted cookie. After this point, it's easy to test the role membership of the currently logged-in user.
For example, the following code checks if the user is an Administrator. In order for this to work, the user must have logged in:
If Roles.IsUserInRole("Administrator") Then ' (Allow the code to continue, or show some content ' that would otherwise be hidden or disabled.) End If
And, finally, this code displays a list of all the roles that the user belongs to:
For Each Role As String In Roles.GetRolesForUser( ) lblRoles.Text &= Role & " " Next
Clearly, none of these tasks requires much work!
You can also set and retrieve role information using the System.Web.Security.Roles class. Here are the core methods you'll want to use:
- CreateRole( )
- This creates a new role with the designated name in the database. Remember, roles are just labels (like Administrator or Guest). It's up to your code to decide how to respond to that information.
- DeleteRole( )
- Removes a role from the data source.
- AddUserToRole( )
- Adds a record in the data store that indicates that the specified user is a member of the specified role. You can also use other methods that work with arrays and allow you to add a user to several different roles at once, or add several different users to the same role. These methods include AddUserToRoles( ), AddUsersToRole(), and AddUsersToRoles( ).
- RemoveUserFromRole( )
- Removes a user from a role.
- GetRolesForUser( )
- Retrieves an array of strings that indicate all the roles a specific user belongs to. If you're retrieving roles for the currently logged-in user, you don't need to specify a username.
- GetUsersInRole( )
- Retrieves an array of strings with all of the usernames that are in a given role.
- IsUserInRole( )
- Tests if a user is in a specific role. This is the cornerstone of role-based authorization. Depending on whether this method returns True or False, your code should decide to allow or restrict certain actions. If you're testing the group membership of the currently logged-in user, you don't need to specify a username.
The following code snippet creates a role and adds a user to that role:
Roles.CreateRole("Administrator") Roles.AddUserToRole("testUser", "Administrator")
...performance? At first glance, role management might not seem very scalable. Reading the role information for each web request is sure to slow down the speed of your application, and it may even introduce a new bottleneck as ASP.NET threads wait to get access to the database. Fortunately, the role-management service is quite intelligent. It won't make a trip to the database with each web request; instead, it retrieves role information once, encrypts it, and stores it in a cookie. For all subsequent requests, ASP.NET reads the roles from the encrypted cookie. You can remove this cookie at any time by calling Roles.DeleteCookie(), or you can configure settings in the web.config file to determine when it should expire on its own.
If you have an extremely large number of roles, the cookie might not contain them all. In this case, ASP.NET flags the cookie to indicate this fact. When your code performs a role check, ASP.NET will try to match one of the roles in the cookie first, and if it can't find a match, it will double-check the data source next.
Where can I learn more?
For more information, look up the index entry "role-based security → ASP.NET" in the MSDN Help.
Store Personalized Information
ASP.NET applications often have the need to store user-specific information beyond the bare minimum username and password. One way to solve this problem is to use the Session collection. Session state has two limitations: it isn't permanent (typically, a session times out after 20 minutes of inactivity), and it isn't strongly typed (in other words, you need to know what's in the session collection and manually cast references to the appropriate data types). ASP.NET 2.0 addresses these limitations with a new framework for storing user-specific data called profile settings.
Need to store some custom user-specific information for long periods of time? Why not use the membership data provider to save and retrieve information without resorting to database code.
How do I do that?
Profiles build on the same provider model that's used for membership and role management. Essentially, the profile provider takes care of storing all the user-specific information in some backend data store. Currently, ASP.NET includes a profile provider that's tailored for SQL Server.
Before you start using profiles, you should have a system in place for authenticating users. That's because personalized information needs to be linked to a specific user, so that you can retrieve it on subsequent visits. Typically, you'll use forms authentication, with the help of the ASP.NET membership services described in the lab "Easily Authenticate Users."
With profiles, you need to define the type of user-specific information you want to store. In early builds, the WAT included a tool for generating profile settings. However, this tool has disappeared in later releases, and unless (or until) it returns, you need to define your profile settings in the web.config file by hand. Here's an example of a profile section that defines a single string named Fullname:
ASP. NET does include basic features that allow you to use personalization with anonymous users (see the "What about..." section of this lab for more information).
<?xml version="1.0"?> <configuration> <system.web> <profile> <properties> <add name="FullName" type="System.String" /> </properties> </profile> <!-- Other settings ommitted. --> </system.web> </configuration>
Initially, this doesn't seem any more useful than an application setting. However, Visual Studio automatically generates a new class based on your profile settings. You can access this class through the Page.Profile property. The other benefit is the fact that ASP.NET stores this information in a backend database, automatically retrieving it from the database at the beginning of the request and writing it back at the end of the request (if these operations are needed). In other words, profiles give you a higher-level model for maintaining user-specific information that's stored in a database.
In other words, assuming you've defined the FullName property in the <profile> section, you can set and retrieve a user's name information using code like this:
Profile.FullName = "Joe Smythe" ... lblName.Text = "Hello " & Profile.FullName
Note that the Profile class is strongly typed. There's no need to convert the reference, and Visual Studio's IntelliSense springs into action when you type Profile followed by the period.
Life gets even more interesting if you want to store a full-fledged object. For example, imagine you create specialized classes to track the products in a user's shopping basket. Example 4-6 shows a Basket class that contains a collection of BasketItem objects, each representing a separate product.
Example 4-6. Custom classes for a shopping cart
Imports System.Collections.Generic Public Class Basket Private _Items As New List(Of BasketItem) Public Property Items( ) As List(Of BasketItem) Get Return _Items End Get Set(ByVal value As List(Of BasketItem)) _Items = value End Set End Property End Class Public Class BasketItem Private _Name As String Public Property Name( ) As String Get Return _Name End Get Set(ByVal value As String) _Name = value End Set End Property Private _ID As String = Guid.NewGuid( ).ToString( ) Public Property ID( ) As String Get Return _ID End Get Set(ByVal value As String) _ID = value End Set End Property Public Sub New(ByVal name As String) _Name = name End Sub Public Sub New( ) ' Used for serialization. End Sub End Class
To use this class, you need to add it to the Code subdirectory so that it's compiled automatically. Then, to make it a part of the user profile, you need to define it in the web.config file, like this:
<profile> <properties> <add name="Basket" type="Basket" /> </properties> </profile>
With this information in place, it's easy to create a simple shopping cart test page. Figure 4-16 shows an example that lets you add and remove items. When the page is first loaded, it checks if there is a shopping basket for the current user, and if there isn't, it creates one. The user can then add items to the cart or remove existing items, using the Add and Remove buttons. Finally, the collection of shopping basket items is bound to a listbox every time the page is rendered, ensuring the page shows the current list of items in the basket. Example 4-7 shows the complete code.
Example 4-7. Testing a personalized shopping basket
<%@ Page language="VB" %> <script runat="server"> Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) If Profile.Basket Is Nothing Then Profile.Basket = New Basket( ) End Sub ' Put a new item in the basket. Sub cmdAdd_Click(ByVal sender As Object, ByVal e As System.EventArgs) Profile.Basket.Items.Add(New BasketItem(txtItemName.Text)) End Sub ' Remove the selected item. Sub cmdRemove_Click(ByVal sender As Object, ByVal e As System.EventArgs) For Each Item As BasketItem In Profile.Basket.Items If Item.ID = lstItems.SelectedItem.Value Then Profile.Basket.Items.Remove(Item) Return End If Next End Sub ' The page is being rendered. Create the list using data binding. Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) lstItems.DataSource = Profile.Basket.Items lstItems.DataTextField = "Name" lstItems.DataValueField = "ID" lstItems.DataBind( ) End Sub </script> <html> <head runat="server"> <title>Test Page</title> </head> <body> <form id="form1" runat="server"> <br /> <br /> <asp:ListBox ID="lstItems" Runat="server" Width="266px" Height="106px"></asp:ListBox><br /> <asp:TextBox ID="txtItemName" Runat="server" Width="266px"></asp:TextBox><br /> <asp:Button ID="cmdAdd" Runat="server" Width="106px" Text="Add New Item" OnClick="cmdAdd_Click" /> <asp:Button ID="cmdRemove" Runat="server" Width="157px" Text="Remove Selected Item" OnClick="cmdRemove_Click" /> </form> </body> </html>
Remember, profile information doesn't time out. That means that even if you rebuild and restart the web application, the shopping cart items will still be there, unless your code explicitly clears them. This makes profiles perfect for storing permanent user-specific information without worrying about the hassle of ADO.NET code.
...anonymous users? By default, you can only access profile information once a user has logged in. However, many web sites retain user-specific information even when users aren't logged in. For example, most online e-commerce shops let users start shopping immediately, and only force them to log in at checkout time. To implement this design (without resorting to session state), you need to use another new feature in ASP.NET—anonymous identification.
With anonymous identification, ASP.NET assigns a unique ID to every new user. This ID is stored in a persistent cookie, which means that even if a user waits several days before making a repeat visit, ASP.NET will still be able to identify the user and find the personalized information from the user's last visit. (The default expiration settings remove the cookie after about one week if the user hasn't returned.)
In order to use anonymous identification, you need to add the <anonymousIdentification> tag to the web.config file, and you need to explicitly indicate what profile information can be tracked anonymously by flagging these properties with the allowAnonymous attribute.
Here's an example with a revised web.config that stores shopping basket information for anonymous users:
<?xml version="1.0"?> <configuration> <system.web> <anonymousIdentification enabled="true"> <profile> <properties> <add name="Basket" type="Basket" allowAnonymous="true"/> </properties> </profile> <!-- Other settings ommitted. --> </system.web> </configuration>
Anonymous identification raises a few new considerations. The most significant occurs in systems where an anonymous user needs to log in at some point to complete an operation. In order to make sure information isn't lost, you need to handle the PersonalizationModule.MigrateAnonymous event in the global.asax file. You can then transfer information from the anonymous profile to the new authenticated profile.
Where can I learn more?
For more information about various profile options, including transferring anonymous profile information into an authenticated profile, look for the index entry "profiles" in the MSDN Help.