Using Session State


You can't really use a cookie to store a shopping cart. A cookie is just too small and too simple. To enable you to work around the limitations of cookies, the ASP.NET Framework supports a feature called Session state.

Like cookies, items stored in Session state are scoped to a particular user. You can use Session state to store user preferences or other user-specific data across multiple page requests.

Unlike cookies, Session state has no size limitations. If you had a compelling need, you could store gigabytes of data in Session state.

Furthermore, unlike cookies, Session state can represent more complex objects than simple strings of text. You can store any object in Session state. For example, you can store a DataSet or a custom shopping cart object in Session state.

You add items to Session state by using the Session object. For example, the page in Listing 22.9 adds a new item named message to Session state that has the value Hello World!.

Listing 22.9. SessionSet.aspx

<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Sub Page_Load()         Session("message") = "Hello World!"     End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head  runat="server">     <title>Session Set</title> </head> <body>     <form  runat="server">     <div>     <h1>Session item added!</h1>     </div>     </form> </body> </html> 

In the Page_Load() event handler in Listing 22.9, a new item is added to the Session object. Notice that you can use the Session object just as you would use a Hashtable collection.

The page in Listing 22.10 illustrates how you can retrieve the value of an item that you have stored in Session state.

Listing 22.10. SessionGet.aspx

<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Sub Page_Load()         lblMessage.Text = Session("message").ToString()     End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head  runat="server">     <title>Session Get</title> </head> <body>     <form  runat="server">     <div>     <asp:Label                  Runat="server" />     </div>     </form> </body> </html> 

When you use Session state, a session cookie named ASP.NET_SessionId is added to your browser automatically. This cookie contains a unique identifier. It is used to track you as you move from page to page.

When you add items to the Session object, the items are stored on the web server and not the web browser. The ASP.NET_SessionId cookie is used to associate the correct data with the correct user.

By default, if cookies are disabled, Session state does not work. You don't receive an error, but items that you add to Session state aren't available when you attempt to retrieve them in later page requests. (You learn how to enable cookieless Session state later in this section.)

Warning

Be careful not to abuse Session state by overusing it. A separate copy of each item added to Session state is created for each user who requests the page. If you place a DataSet with 400 records into Session state in a page, and 500 users request the page, then you'll have 500 copies of that DataSet in memory.


By default, the ASP.NET Framework assumes that a user has left the website when the user has not requested a page for more than 20 minutes. At that point, any data stored in Session state for the user is discarded.

Storing Database Data in Session State

You can use Session state to create a user-relative cache. For example, you can load data for a user and enable the user to sort or filter the data.

The page in Listing 22.11 loads a DataView into Session state. The user can sort the contents of the DataView by using a GridView control (see Figure 22.4).

Listing 22.11. SessionDataView.aspx

[View full width]

<%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Dim dvMovies As DataView     ''' <summary>     ''' Load the Movies     ''' </summary>     Private Sub Page_Load()         dvMovies = CType(Session("Movies"), DataView)         If IsNothing(dvMovies) Then             Dim conString As String = WebConfigurationManager.ConnectionStrings("Movies"). ConnectionString             Dim dad As New SqlDataAdapter("SELECT Id,Title,Director FROM Movies", conString)             Dim dtblMovies As New DataTable()             dad.Fill(dtblMovies)             dvMovies = New DataView(dtblMovies)             Session("Movies") = dvMovies         End If     End Sub     ''' <summary>     ''' Sort the Movies     ''' </summary>     Protected Sub grdMovies_Sorting(ByVal sender As Object, ByVal e As GridViewSortEventArgs)         dvMovies.Sort = e.SortExpression     End Sub     ''' <summary>     ''' Render the Movies     ''' </summary>     Private Sub Page_PreRender()         grdMovies.DataSource = dvMovies         grdMovies.DataBind()     End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head  runat="server">     <title>Session DataView</title> </head> <body>     <form  runat="server">     <div>     <asp:GridView                  AllowSorting="true"         EnableViewState="false"         OnSorting="grdMovies_Sorting"         Runat="server" />     <br />     <asp:LinkButton                  Text="Reload Page"         Runat="server" />     </div>     </form> </body> </html> 

Figure 22.4. Sorting a DataView stored in Session state.


In Listing 22.11, a DataView object is stored in Session state. When you sort the GridView control, the DataView is sorted.

The page in Listing 22.11 includes a link that enables you to reload the page. Notice that the sort order of the records displayed by the GridView is remembered across page requests. The sort order is remembered even if you navigate to another page before returning to the page.

Using the Session Object

The main application programming interface for working with Session state is the HttpSessionState class. This object is exposed by the Page.Session, Context.Session, UserControl.Session, WebService.Session, and Application.Session properties. This means that you can access Session state from just about anywhere.

This HttpSessionState class supports the following properties (this is not a complete list):

  • CookieMode Enables you to specify whether cookieless sessions are enabled. Possible values are AutoDetect, UseCookies, UseDeviceProfile, and UseUri.

  • Count Enables you to retrieve the number of items in Session state.

  • IsCookieless Enables you to determine whether cookieless sessions are enabled.

  • IsNewSession Enables you to determine whether a new user session was created with the current request.

  • IsReadOnly Enables you to determine whether the Session state is read-only.

  • Keys Enables you to retrieve a list of item names stored in Session state.

  • Mode Enables you to determine the current Session state store provider. Possible values are Custom, InProc, Off, SqlServer, and StateServer.

  • SessionID Enables you to retrieve the unique session identifier.

  • Timeout Enables you to specify the amount of time in minutes before the web server assumes that the user has left and discards the session. The maximum value is 525,600 (1 year).

The HttpSessionState object also supports the following methods:

  • Abandon Enables you to end a user session.

  • Clear Enables you to clear all items from Session state.

  • Remove Enables you to remove a particular item from Session state.

The Abandon() method enables you to end a user session programmatically. For example, you might want to end a user session automatically when a user logs out from your application to clear away all of a user's session state information.

Handling Session Events

There are two events related to Session state that you can handle in the Global.asax file: the Session Start and Session End events.

The Session Start event is raised whenever a new user session begins. You can handle this event to load user information from the database. For example, you can handle the Session Start event to load the user's shopping cart.

The Session End event is raised when a session ends. A session comes to an end when it times out because of user inactivity or when it is explicitly ended with the Session.Abandon() method. You can handle the Session End event, for example, when you want to automatically save the user's shopping cart to a database table.

The Global.asax file in Listing 22.12 demonstrates how you can handle both the Session Start and End events.

Listing 22.12. Global.asax

<%@ Application Language="VB" %> <script runat="server">     Private Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)         Application("SessionCount") = 0     End Sub     Private Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)         Application.Lock()         Dim count As Integer = CType(Application("SessionCount"), Integer)         Application("SessionCount") = count + 1         Application.UnLock()     End Sub     Private Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)         Application.Lock()         Dim count As Integer = CType(Application("SessionCount"), Integer)         Application("SessionCount") = count - 1         Application.UnLock()     End Sub </script> 

In Listing 22.12, the Global.asax file is used to track the number of active sessions. Whenever a new session begins, the Session Start event is raised and the SessionCount variable is incremented by one. When a session ends, the Session End event is raised and the SessionCount variable is decremented by one.

The SessionCount variable is stored in Application state. Application state contains items that are shared among all users of a web application. Notice that the Application object is locked before it is modified. You must lock and unlock the Application object because multiple users could potentially access the same item in Application state at the same time.

Note

Application state is little used in ASP.NET applications. In most cases, you should use the Cache object instead of Application state because the Cache object is designed to manage memory automatically.


The page in Listing 22.13 displays the number of active sessions with a Label control (see Figure 22.5).

Figure 22.5. Displaying a count of user sessions.


Listing 22.13. ShowSessionCount.aspx

<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Sub Page_Load()         lblSessionCount.Text = Application("SessionCount").ToString()     End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head  runat="server">     <title>Show Session Count</title> </head> <body>     <form  runat="server">     <div>     Total Application Sessions:     <asp:Label                  Runat="server" />     </div>     </form> </body> </html> 

Warning

The Session End event is not raised by all session store providers. The event is raised by the InProc session store provider (the default provider), but it is not raised by the StateServer or SQLServer state providers.


Controlling When a Session Times Out

By default, the ASP.NET Framework assumes that a user has left an application after 20 minutes have passed without the user requesting a page. In some situations, you'll want to modify the default timeout value.

For example, imagine that you are creating a college admissions website and the website includes a form that enables an applicant to enter a long essay. In that situation, you would not want the user session to timeout after 20 minutes. Please, give the poor college applicants at least an hour to write their essays.

The disadvantage of increasing the Session timeout is that more memory is consumed by your application. The longer the Session timeout, the more server memory is potentially consumed.

You can specify the Session timeout in the web configuration file or you can set the Session timeout programmatically. For example, the web configuration file in Listing 22.14 changes the Session timeout value to 60 (one hour).

Listing 22.14. Web.Config

<?xml version="1.0"?> <configuration> <system.web>   <sessionState timeout="60" /> </system.web> </configuration> 

You can modify the Session timeout value programmatically with the Timeout property of the Session object. For example, the following statement changes the timeout value from the default of 20 minutes to 60 minutes.

Session.Timeout = 60 


After you execute this statement, the timeout value is modified for the remainder of the user session. This is true even when the user visits other pages.

Using Cookieless Session State

By default, Session state depends on cookies. The ASP.NET Framework uses the ASP.NET_SessionId cookie to identity a user across page requests so that the correct data can be associated with the correct user. If a user disables cookies in the browser, then Session state doesn't work.

If you want Session state to work even when cookies are disabled, then you can take advantage of cookieless sessions. When cookieless sessions are enabled, a user's session ID is added to the page URL.

Here's a sample of what a page URL looks like when cookieless sessions are enabled:

http://localhost:4945/Original/(S(5pnh11553sszre45oevthxnn))/SomePage.aspx 


The strange-looking code in this URL is the current user's Session ID. It is the same value as the one you get from the Session.SessionID property.

You enable cookieless sessions by modifying the sessionState element in the web configuration file. The sessionState element includes a cookieless attribute that accepts the following values:

  • AutoDetect The Session ID is stored in a cookie when a browser has cookies enabled. Otherwise, the cookie is added to the URL.

  • UseCookies The Session ID is always stored in a cookie (the default value).

  • UseDeviceProfile The Session ID is stored in a cookie when a browser supports cookies. Otherwise, the cookie is added to the URL.

  • UseUri The Session ID is always added to the URL.

When you set cookieless to the value UseDeviceProfile, the ASP.NET Framework determines whether the browser supports cookies by looking up the browser's capabilities from a set of files contained in the following folder:

\WINDOWS\Microsoft.NET\Framework\[version]\CONFIG\Browsers 


If, according to these files, a browser supports cookies, then the ASP.NET Framework uses a cookie to store the Session ID. The Framework attempts to add a cookie even when a user has disabled cookies in the browser.

When cookieless is set to the value AutoDetect, the framework checks for the existence of the HTTP Cookie header. If the Cookie header is detected, then the framework stores the Session ID in a cookie. Otherwise, the framework falls back to storing the Session ID in the page URL.

The web configuration file in Listing 22.15 enables cookieless sessions by assigning the value AutoDetect to the cookieless attribute.

Listing 22.15. Web.Config

<?xml version="1.0"?> <configuration> <system.web>   <sessionState     cookieless="AutoDetect"     regenerateExpiredSession /> </system.web> </configuration> 

Note

The easiest way to test cookieless sessions is to use the Mozilla Firefox browser because this browser enables you to disable cookies easily. Select the menu option Tools, Options. Select the Privacy tab and uncheck Allow Sites to Set Cookies.


Notice that the configuration file in Listing 22.16 also includes a regenerateExpiredSessionId attribute. When you enable cookieless session state, you should also enable this attribute because it can help prevent users from inadvertently sharing session state.

For example, imagine that someone posts a link in a discussion forum to an ASP.NET website that has cookieless sessions enabled. The link includes the Session ID. If someone follows the link after the original session has timed out, then a new Session is started automatically. However, if multiple people follow the link at the same time, then all the people will share the same Session ID and, therefore, they will share the same Session state, which is a major security problem.

On the other hand, when regenerateExpiredSessionId is enabled and a session times out, the Session ID in the URL is regenerated when a person requests the page. A redirect back to the same page is performed to change the Session ID in the URL. If a link is posted in a discussion forum, or sent to multiple users in an email, then each user who follows the link is assigned a new Session ID.

When you enable cookieless sessions, you need to be careful to use relative URLs when linking between pages in your application. If you don't use a relative URL, then the Session ID cannot be added to the URL automatically.

For example, when linking to another page in your website, use a URL that looks like this (a relative URL):

/SomeFolder/SomePage.aspx 


Do not use a URL that looks like this (an absolute URL):

http://SomeSite.com/SomeFolder/SomePage.aspx 


If, for some reason, you really need to use an absolute URL, you can add the Session ID to the URL by using the Response.ApplyAppPathModifier() method. This method takes an absolute URL and returns the URL with a Session ID embedded in it.

Configuring a Session State Store

By default, Session state is stored in memory in the same process as the ASP.NET process. There are two significant disadvantages to storing Session state in the ASP.NET process.

First, in-process Session state is fragile. If your application restarts, then all Session state is lost. A number of different events can cause an application restart. For example, modifying the web configuration file or errors in your application both can cause an application restart.

Second, in-process Session state is not scalable. When Session state is stored in-process, it is stored on a particular web server. In other words, you can't use in-process Session state with a web farm.

If you need to implement a more robust version of Session state, then the ASP.NET Framework supplies you with a number of options. You can configure the ASP.NET Framework to store Session state in an alternate location by modifying the Session state mode.

You can set the Session state mode to any of the following values:

  • Off Disables Session state.

  • InProc Stores Session state in the same process as the ASP.NET process.

  • StateServer Stores Session state in a Windows NT process, which is distinct from the ASP.NET process.

  • SQLServer Stores Session state in a SQL Server database.

  • Custom Stores Session state in a custom location.

By default, the Session state mode is set to the value InProc. This is done for performance reasons. In-process Session state results in the best performance. However, it sacrifices robustness and scalability.

When you set the Session state mode to either StateServer or SQLServer, you get robustness and scalability at the price of performance. Storing Session state out-of-process results in worse performance because Session state information must be passed back and forth over your network.

Finally, you can create a custom Session state store provider by inheriting a new class from the SessionStateStoreProviderBase class. In that case, you can store Session state any place that you want. For example, you can create a Session state store provider that stores Session state in an Oracle or FoxPro database.

Configuring State Server Session State

When you enable State Server Session state, Session state information is stored in a separate Windows NT Service. The Windows NT Service can be located on the same server as your web server, or it can be located on another server in your network.

If you store Session state in the memory of a separate Windows NT Service, then Session state information survives even when your ASP.NET application doesn't. For example, if your ASP.NET application crashes, then your Session state information is not lost because it is stored in a separate process.

Furthermore, you can create a web farm when you store state information by using a Windows NT Service. You can designate one server in your network as your state server. All the web servers in your web farm can use the central state server to store Session state.

You must complete the following two steps to use State Server Session state:

  • Start the ASP.NET State Service.

  • Configure your application to use the ASP.NET State Service.

You can start the ASP.NET State Service by opening the Services applet located at Start, Control Panel, Administrative Tools (see Figure 22.6). After you open the Services applet, double-click the ASP.NET State Service and click Start to run the service. You also should change the Startup type of the service to the value Automatic so that the service starts automatically every time that you reboot your machine.

Figure 22.6. Starting the ASP.NET State service.


If you want to run the ASP.NET State Service on a separate server on your network, then you must edit a Registry setting on the server that hosts the ASP.NET State Service. By default, the ASP.NET State Service does not accept remote connections. To allow remote connections, execute RegEdit from a command prompt and set the following Registry key to the value 1:

[View full width]

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters \AllowRemoteConnection


After you start the ASP.NET State Service, you need to configure your ASP.NET application to use it. The web configuration file in Listing 22.16 enables State Server Session State.

Listing 22.16. Web.Config

[View full width]

<?xml version="1.0"?> <configuration>     <system.web>       <sessionState         mode="StateServer"         stateConnectionString="tcpip=localhost:42424"         stateNetworkTimeout="10"  />       <machineKey         decryption="AES"         validation="SHA1"         decryptionKey="306C1FA852AB3B0115150DD8BA30821CDFD125538A0C606DACA53DBB3C3E0AD2"         validationKey="61A8E04A146AFFAB81B6AD19654F99EA7370807F18F5002725DAB98B8EFD19C7113 37E26948E26D1D174B159973EA0BE8CC9CAA6AAF513BF84E44B2247792265" />     </system.web> </configuration> 

The web configuration file in Listing 22.16 modifies three attributes of the sessionState element. First, the mode attribute is set to the value StateServer. Next, the stateConnectionString attribute is used to specify the location of the ASP.NET State Server. In Listing 22.16, a connection is created to the local server on port 42424. Finally, the stateNetworkTimeout attribute is used to specify a connection timeout in seconds.

Note

You can configure the ASP.NET State Server to use a different port by modifying the following Registry value:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters\Port 


You need to stop and restart the ASP.NET State Service with the Services applet after making this modification.


Notice that the web configuration in Listing 22.17 includes a machineKey element. If you are setting up a web farm, and you need to use the same State Server to store Session state for multiple servers, then you are required to specify explicit encryption and validation keys. On the other hand, you don't need to include a machineKey element when the ASP.NET State Server is hosted on the same machine as your ASP.NET application.

Warning

Don't use the web configuration file in Listing 22.17 without modifying the values of both the decryptionKey and validationKey attributes. Those values must be secret. You can use the GenerateKeys.aspx page discussed in the previous chapter (Chapter 21, "Using ASP.NET Membership") to generate new values for these attributes.


After you complete these configuration steps, Session state information is stored in the ASP.NET State Server automatically. You don't need to modify any of your application code when you switch to out-of-process Session state.

Configuring SQL Server Session State

If you want to store Session state in the most reliable way possible, then you can store Session state in a Microsoft SQL Server database. Because you can set up failover SQL Server clusters, Session state stored in SQL Server should be able to survive just anything, including a major nuclear war.

You must complete the following two steps to enable SQL Server Session state:

  • Configure your database to support SQL Server Session state.

  • Configure your application to use SQL Server Session state.

You can use the aspnet_regsql tool to add the necessary tables and stored procedures to your database to support SQL Server Session state. The aspnet_regsql tool is located in the following path:

\WINDOWS\Microsoft.NET\Framework\[version]\aspnet_regsql.exe 


Note

If you open the SDK Command Prompt, you don't need to navigate to the Microsoft.NET folder to use the aspnet_regsql tool.


Executing the following command enables SQL Server Session state for a database server named YourServer.

aspnet_regsql -C "Data Source=YourServer;Integrated Security=True" -ssadd 


When you execute this command, a new database is created on your database server named ASPState. The ASPState database contains all the stored procedures used by Session state. However, by default, Session state information is stored in the TempDB database. When your database server restarts, the TempDB database is cleared automatically.

If you want to use SQL Server Session state with a failover cluster of SQL Servers, then you can't store Session state in the TempDB database. Also, if you want Session state to survive database restarts, then you can't store the state information in the TempDB database.

If you execute the following command, then Session state is stored in the ASPState database instead of the TempDB database:

aspnet_regsql -C "Data Source=YourServer;Integrated Security=True" -ssadd -sstype p 


Notice that this command includes a -sstype p switch. The p stands for persistent. Session state that is stored in the ASPState database is called persistent Session state because it survives database server restarts.

Finally, you can store Session state in a custom database. The following command stores Session state in a database named MySessionDB:

[View full width]

aspnet_regsql -C "Data Source=YourServer;Integrated Security=True" -ssadd -sstype c -d MySessionDB


Executing this command creates a new database named MySessionDB that contains both the tables and stored procedures for storing Session state. Notice that the -sstype switch has the value c for custom. The command also includes a -d switch that enables you to specify the name of the new database.

If you want to remove the Session state tables and stored procedures from a server, then you can execute the following command:

aspnet_regsql -C "Data Source=YourServer;Integrated Security=True" -ssremove 


Executing this command removes the ASPState database. It does not remove a custom Session state database. You must remove a custom database manually.

After you configure your database server to support Session state, you must configure your ASP.NET application to connect to your database. You can use the web configuration file in Listing 22.17 to connect to a database named YourServer.

Listing 22.17. Web.Config

[View full width]

<?xml version="1.0"?> <configuration>     <system.web>       <sessionState         mode="SQLServer"         sqlConnectionString="Data Source=YourServer;Integrated Security=True"         sqlCommandTimeout="30" />       <machineKey         decryption="AES"         validation="SHA1"         decryptionKey="306C1FA852AB3B0115150DD8BA30821CDFD125538A0C606DACA53DBB3C3E0AD2"         validationKey="61A8E04A146AFFAB81B6AD19654F99EA7370807F18F5002725DAB98B8EFD19C7113 37E26948E26D1D174B159973EA0BE8CC9CAA6AAF513BF84E44B2247792265" />     </system.web> </configuration> 

The sessionState element includes three attributes. The mode attribute is set to the value SQLServer to enable SQL Server Session state. The second attribute, sqlConnectionString, contains the connection string to the Session state database. Finally, the sqlCommandTimeout specifies the maximum amount of time in seconds before a command that retrieves or stores Session state times out.

Notice that the configuration file in Listing 22.18 includes a machineKey element. If your Session state database is located on a different machine than your ASP.NET application, then you are required to include a machineKey element that contains explicit encryption and validation keys.

Warning

Don't use the web configuration file in Listing 22.17 without modifying the values of both the decryptionKey and validationKey attributes. Those values must be secret. You can use the GenerateKeys.aspx page discussed in the previous chapter (Chapter 21, "Using ASP.NET Membership") to generate new values for these attributes.


If you select the option to store Session state in a custom database when executing the aspnet_regsql tool, then you need to specify the name of the custom database in your configuration file. You can use the web configuration file in Listing 22.18.

Listing 22.18. Web.config

[View full width]

<?xml version="1.0"?> <configuration>     <system.web>       <sessionState         mode="SQLServer"         sqlConnectionString="Data Source=YourServer; Integrated Security=True;database=MySessionDB"         sqlCommandTimeout="30"         allowCustomSqlDatabase="true"/>       <machineKey         decryption="AES"         validation="SHA1"         decryptionKey="306C1FA852AB3B0115150DD8BA30821CDFD125538A0C606DACA53DBB3C3E0AD2"         validationKey="61A8E04A146AFFAB81B6AD19654F99EA7370807F18F5002725DAB98B8EFD19C7113 37E26948E26D1D174B159973EA0BE8CC9CAA6AAF513BF84E44B2247792265" />     </system.web> </configuration> 

The sessionState element in the configuration file in Listing 22.18 includes an allowCustomSqlDatabase attribute. Furthermore, the sqlConnectionString attribute contains the name of the custom database.

Enabling SQL Server session state has no effect on how you write your application code. You can initially build your application using in-process Session state and, when you have the need, you can switch to SQL Server Session state.




ASP. NET 2.0 Unleashed
ASP.NET 2.0 Unleashed
ISBN: 0672328232
EAN: 2147483647
Year: 2006
Pages: 276

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