MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual Basic .NET and the .NET Framework SDK
Brian A. Randell
MCW Technologies, LLC
March 2004
Applies to:
Microsoft® .NET Framework SDK
Microsoft® ASP.NET version 1.1
Microsoft® SQL Server™ 2000 Desktop Engine (MSDE 2000)
Microsoft® Visual Basic® .NET
Summary: Create a data-driven website with MSDE and ASP.NET 1.1,and Visual Basic .NET code. (44 printed pages)
Download the associated VBSDKSupport.exe walkthrough code sample.
Download the complete Visual Basic and the .NET Framework SDK Sample. To use the sample, create the Pics2Share database, following the instructions in the Building the Pics2Share Sample Database-3.rtf file. Then run the .msi file to install the sample application.
Contents
Introduction
Prerequisites
Getting Started
Building the Data Access Layer
Building the Image Upload Facility
Generating the Thumbnail Images
Building the Main Page
Enabling Full Size Image Display
Adding Security
Tracking Sessions
Error Handling
Conclusion
Appendix A: Changing File System Permissions
Additional Links
Related Books
Introduction
In this walkthrough, you will iteratively build a Web application that displays pictures from the file system and picture metadata from an MSDE database. The application will support dynamically generated pages based upon whether the user is anonymous or not. Authenticated users will be filtered by their roles.
Prerequisites
To perform this walkthrough, the following software and components must be installed on the development computer:
A text editor, such as Notepad, to create the application's source files
Microsoft SQL Server 2000 Desktop Engine (MSDE) Release A
The sample Pics2Share database. See the Building the Pics2Share Sample Database.rtf file for instructions, which is available in the downloadable code sample listed at the start of this article.
The codevbsdk.txt file containing source code, HTML, etc., necessary to build the solution, and the denied.gif, noimage.gif and BuildAll.bat files, available in the downloadable code sample listed at the start of this article.
**Tip **Although it's not required, this demonstration assumes that you've added the Option Strict statement to each module in your project. Setting the Option Strict setting to On requires a bit more code, as you'll see, but it also ensures that you don't perform any unsafe type conversions. You can get by without it, but in the long run, the discipline required to take advantage of this option will far outweigh the difficulties it adds to writing code.
Getting Started
To get started, you will create a directory for your project.
To create an ASP.NET Web Application folder and virtual directory
- Using Windows Explorer or a command prompt, create a folder called mypics under C:\Inetpub\wwwoot.
- Start the Internet Information Services program.
- Navigate to the Default Web Site node for your local computer.
- Right-click, and on the context menu, select New, Virtual Directory. On the wizard introduction page, click Next.
- In the Alias field, type mypics and then click Next.
- In the Directory field, enter C:\Inetpub\wwwroot\mypics, click Next twice, and then click Finish.
- Close the Internet Information Services program.
Creating a Style Sheet
In order to give the application a consistent look and feel, you will use a cascading style sheet (CSS) file:
- Create a new file named Styles.css.
- If you've not already done so, open the codevbsdk.txt file.
- Copy the entire text of Item 1 from codevbsdk.txt to the clipboard.
- In the Styles.css file, paste the text from the clipboard.
- Save and close the Styles.css file.
Building the Data Access Layer
In this part you will build a class to perform all of your data access, isolating the data access code for convenience and future ease of maintenance. First you will add a database connection string to your web.config file for later retrieval.
To add a database connection string
Create a configuration file for your application named web.config.
Put the following XML (which should be copied from codevbsdk.txt, Item 2) into the file. There should be no line breaks in the quoted string provided for value:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <appSettings> <add key="ConnectionString" value= "Server=localhost;Database=Pics2Share; Trusted_Connection=True;Connection Timeout=60; Pooling=True;Min Pool Size=1;Max Pool Size=5"/> </appSettings> <system.web> </system.web> </configuration>
Note The configuration information above assumes you've installed the sample database, Pics2Share, on the default instance of MSDE 2000 (or SQL Server 2000) on the same machine as your Web server. If this is not the case, you will need to modify connection string, possibly adjusting the security settings also.
Save and close the web.config file.
To create the data access class
Create a file called SSDAL.vb
Create a public class named SSDAL in a namespace called MyPics.
At the top of the file, add the following Imports directives:
Imports System Imports System.Configuration Imports System.Data Imports System.Data.SqlClient
Add the following shared function to your new SSDAL class (Item 3 in codevbsdk.txt):
Public Shared Function AddImage(ByVal ImageName As String, _ ByVal ImageDesc As String, _ ByVal ImagePath As String, _ ByVal ImageThumb As String, _ ByVal UserId As Integer, _ ByVal MinRole As Integer, _ ByVal ImageGroupId As Integer) _ As Integer Dim retVal As Integer = -1 Dim mcon As SqlConnection Dim mcmd As SqlCommand Try Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") mcon = New SqlConnection(conString) mcmd = New SqlCommand("AddImageMetaData", mcon) mcmd.CommandType = CommandType.StoredProcedure Dim prm As SqlParameter prm = New SqlParameter("@ImageName", SqlDbType.VarChar, 255) prm.Value = ImageName mcmd.Parameters.Add(prm) prm = New SqlParameter("@ImageDesc", SqlDbType.VarChar, 255) prm.Value = ImageDesc mcmd.Parameters.Add(prm) prm = New SqlParameter("@ImagePath", SqlDbType.VarChar, 255) prm.Value = ImagePath mcmd.Parameters.Add(prm) prm = New SqlParameter("@ImageThumb", SqlDbType.VarChar, 255) prm.IsNullable = True If ImageThumb Is Nothing Then prm.Value = DBNull.Value Else prm.Value = ImageThumb End If mcmd.Parameters.Add(prm) prm = New SqlParameter("@UserId", SqlDbType.Int) prm.Value = UserId mcmd.Parameters.Add(prm) prm = New SqlParameter("@MinRole", SqlDbType.Int) prm.Value = MinRole mcmd.Parameters.Add(prm) prm = New SqlParameter("@ImageGroupId", SqlDbType.Int) prm.Value = ImageGroupId mcmd.Parameters.Add(prm) prm = New SqlParameter("RETURN_VALUE", SqlDbType.Int) prm.Direction = ParameterDirection.ReturnValue mcmd.Parameters.Add(prm) mcon.Open() mcmd.ExecuteNonQuery() retVal = CType(mcmd.Parameters("RETURN_VALUE").Value, Integer) Return retVal Finally If Not mcmd Is Nothing Then mcmd.Dispose() mcmd = Nothing End If If Not mcon Is Nothing Then If mcon.State = ConnectionState.Open Then mcon.Close() End If mcon = Nothing End If End Try End Function
Add the following shared property to your class (Item 4):
Public Shared ReadOnly Property ImageGroups() As DataTable Get Dim dt As DataTable Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim mcon As SqlConnection = New SqlConnection(conString) Dim mcmd As SqlCommand = _ New SqlCommand("GetAllImageGroups", mcon) mcmd.CommandType = CommandType.StoredProcedure Dim msda As SqlDataAdapter = New SqlDataAdapter(mcmd) Dim ds As DataSet = New DataSet msda.Fill(ds, "AllImageGroups") dt = ds.Tables(0) Return dt End Get End Property
Finally add the following shared property to your class (Item 5):
Public Shared ReadOnly Property UserRoles() As DataView Get Dim retVal As DataView = Nothing Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim mcon As SqlConnection = New SqlConnection(conString) Dim mcmd As SqlCommand = _ New SqlCommand("GetAllUserRoles", mcon) mcmd.CommandType = CommandType.StoredProcedure Dim msda As SqlDataAdapter = New SqlDataAdapter(mcmd) Dim ds As New DataSet("UserRoles") msda.Fill(ds, "AllUserRoles") If ds.Tables(0).Rows.Count > 0 Then retVal = ds.Tables(0).DefaultView End If Return retVal End Get End Property
Building the Image Upload Facility
In this section, you will build the facility to upload images to your Web application.
Building the Upload Form
Create a new file called NewImage.aspx.
Add the following directive as the first line in the file:
<%@ Page Language="VB" Explicit="True" Strict="True" %>
Add the HTML in Item 6 from codevbsdk.txt to setup the page's layout after the script block.
After the @ Page directive and before the <HTML> tag, add the following import directives:
<%@ import Namespace="MyPics" %> <%@ import Namespace="System.IO" %> <%@ import Namespace="System.Data" %>
Next add a script block after the import directives (and before the <HTML> tag):
<script runat="server"> </script>
Next add the following two methods in the script block to populate the two DropDownList controls (Item 7).
Private Sub LoadImageGroups() Dim dv As DataView = New DataView(SSDAL.ImageGroups) ' Perform Data Binding If Not dv Is Nothing Then With Me.cboImageGroups .DataSource = dv .DataValueField = "ImageGroupId" .DataTextField = "ImageGroup" .DataBind() End With End If End Sub Private Sub LoadRoles() Dim dv As DataView = SSDAL.UserRoles ' Perform Data Binding If Not dv Is Nothing Then With Me.cboMinRole .DataSource = dv .DataValueField = "RoleId" .DataTextField = "RoleName" .DataBind() End With End If End Sub
Add a handler in the script block for the Load event of the Page object:
Sub Page_Load(sender As Object, e As EventArgs) End Sub
Now, call the two methods you previously added from your Page_Load method as follows (Item 8):
If Not Page.IsPostBack Then Me.LoadImageGroups() Me.LoadRoles() End If
Create a new file called AppGlobals.vb
Create public class named AppGlobals in a namespace called MyPics.
Add the following constant members to your AppGlobals class (Item 9):
Public Const pathUploads As String = "Uploads" Public Const fileDenied As String = "images/denied.gif" Public Const fileNotFound As String = "images/noimage.gif"
Back in NewImage.aspx, add a handler in the script block for the Click event of the Upload Now Button on your form:
Sub btnUpload_Click(sender As Object, e As EventArgs) End Sub
In the HTML, link the Upload Now Button to the event handler by adding the following attribute:
onclick="btnUpload_Click"
Add the following logic (Item 10) to the handler. This procedure will be enhanced a few more times before the application is complete.
Me.hlinkViewImage.Visible = False Dim strUploadFileName As String = Me.Upfile.PostedFile.FileName Dim strFileNameOnly As String = Path.GetFileName(strUploadFileName) Dim strServerPath As String = Server.MapPath(AppGlobals.pathUploads) If Not strServerPath.EndsWith("\") Then strServerPath &= "\" End If Dim strServerFileName As String = strServerPath & strFileNameOnly Try ' Save the file to disk Me.Upfile.PostedFile.SaveAs(strServerFileName) ' Generate the thumbnail Dim strThumbFile As String = "Thmb" & strFileNameOnly Dim strFullThumbFile As String = strServerPath & strThumbFile ' TODO -- Generate Thumbnail ' TODO -- Once security is enabled, provide the correct user id Dim intImageId As Integer = SSDAL.AddImage(strFileNameOnly, _ Me.txtImageDesc.Text, strServerPath, strThumbFile, 1, _ Convert.ToInt32(Me.cboMinRole.SelectedValue), _ Convert.ToInt32(Me.cboImageGroups.SelectedValue)) If intImageId > 0 Then ' TODO -- Add Encryption hlinkViewImage.NavigateUrl = _ String.Format("ShowImage.aspx?{0}", _ "Path=" & strServerPath & strFileNameOnly) Me.hlinkViewImage.Visible = True End If Catch ex As Exception Me.lblMsg.Text = ex.Message Finally If Me.lblMsg.Text.Length > 0 Then Me.lblMsg.Visible = True End If End Try
Using the Windows Explorer, create a new folder called Uploads under the mypics folder.
**Caution **You must give Modify rights to the account under which ASP.NET is executing to this new directory using the NTFS DACL editor. If you do not, you will receive an exception when your code attempts to save a new image to the hard drive. See the Appendix A: Changing File System Permissions at the end of this walkthrough for instructions.
Repeat the process and create a folder named bin.
Repeat the process one more time and create a folder named Images. Copy the two image files included with this walkthrough, denied.gif and noimage.gif to the newly created Images folder.
Before you can test the page, you need to compile your class files into an assembly. Copy the file BuildAll.bat to same directory where your files are stored and run it. This will create a DLL named MyPics.dll in the bin folder under your application root.
**Caution **The batch file assumes that the .NET Framework 1.1 is installed on your C: drive under a folder named Windows. If this is not the case, you will need to modify the batch file accordingly.
Start your Web browser and navigate to https://localhost/mypics/NewImage.aspx.
You should be able to upload an image. Verify the image exists by looking in the Uploads folder and checking the Images table in MSDE.
Generating the Thumbnail Images
In this section, you will add the ability to generate a thumbnail image for each uploaded image.
Creating an Image Utility Class
Create a new file called ImageUtil.vb
Create public class named ImageUtil in a namespace called MyPics.
Add the following Imports directives to the top of the file:
Imports System Imports System.Drawing
Add a shared function called GenerateThumb to the class as shown below (Item 11):
Public Shared Function GenerateThumb( _ ByVal FilePath As String) As Bitmap ' We've selected 120 pixels as the arbitrary height ' for the thumbnails. The code preserves the size ratio, ' given this height. If you want larger thumbnails, you can ' modify this value. Const THUMBNAIL_HEIGHT As Integer = 120 Dim bmp As Bitmap Try bmp = New Bitmap(FilePath) Dim decRatio As Decimal = _ Convert.ToDecimal(bmp.Width / bmp.Height) Dim intWidth As Integer = _ Convert.ToInt32(decRatio * THUMBNAIL_HEIGHT) Dim img As Image = bmp.GetThumbnailImage(intWidth, _ THUMBNAIL_HEIGHT, AddressOf ThumbnailCallback, IntPtr.Zero) Return CType(img, Bitmap) Catch ex As Exception Return Nothing Finally If Not bmp Is Nothing Then bmp.Dispose() End If End Try End Function
Add one more function to satisfy the callback method required by the GetThumbnailImage method of the Framework's Bitmap class (Item 12):
Private Shared Function ThumbnailCallback() As Boolean ' You have to supply this delegate, even though the thumbnail ' retrieval doesn't actually use it. See the documentation ' for more information. Return False End Function
Now, go back to your NewImage.aspx file and locate the TODO -- Generate Thumbnail comment within the btnUpload_Click handler and add the following logic (Item 13) to generate the thumbnail image immediately after that comment:
Dim bmp As Bitmap = Nothing Try If Not File.Exists(strFullThumbFile) Then bmp = ImageUtil.GenerateThumb(strServerFileName) If Not bmp Is Nothing Then bmp.Save(strFullThumbFile, Imaging.ImageFormat.Jpeg) Else strFullThumbFile = Nothing End If End If Catch ex As Exception strFullThumbFile = Nothing Finally If Not bmp Is Nothing Then bmp.Dispose() End If End Try
Add the following import directive:
<%@ import Namespace="System.Drawing" %>
Compile your assembly by running BuildAll.bat again.
Run your application again and try to upload another image. Verify that an additional image file is generated in the Uploads directory with the "Thmb" prefix.
Building the Main Page
In this section you will build the default page of the application. This page will display the image thumbnails and image metadata to the user five images at a time.
Adding Default.aspx
Now that you have the basic uploading of images working, it is time to build the main display page.
Create a new file called Default.aspx.
Add the following directive as the first line in the file:
<%@ Page Language="VB" Explicit="True" Strict="True" %>
Add the HTML in Item 14 from codevbsdk.txt to setup the page's layout after the script block.
After the @ Page directive and before the <HTML> tag, add the following import directives:
<%@ import Namespace="MyPics" %> <%@ import Namespace="System.Data" %>
Next add a script block after the import directives (and before the <HTML> tag):
<script runat="server"> </script>
Before you add the logic to display data on the page, you need to add another method to the data access layer that will return all of the image information. Open the SSDAL.vb file and add the following read-only shared property to the SSDAL class (Item 15):
Public Shared ReadOnly Property AllImages() As DataTable Get Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim mcon As SqlConnection = New SqlConnection(conString) Dim mcmd As New SqlCommand("GetAllImageData", mcon) mcmd.CommandType = CommandType.StoredProcedure Dim msda As SqlDataAdapter = New SqlDataAdapter(mcmd) Dim ds As DataSet = New DataSet("Images") msda.Fill(ds, "AllImages") Return ds.Tables(0) End Get End Property
In script block of your Default.aspx page, add a new method to your class to load the DataGrid (Item 16):
Private Sub LoadGridData() Dim dv As DataView = New DataView(SSDAL.AllImages) dv.RowFilter = "ImageGroupId = " & cboImageGroups.SelectedValue Me.grdImages.DataSource = dv Me.grdImages.DataBind() End Sub
Add another method to populate the Image groups DropDownList (Item 17):
Private Sub LoadImageGroups() Dim dv As DataView = New DataView(SSDAL.ImageGroups) If Not dv Is Nothing Then With Me.cboImageGroups .DataSource = dv .DataValueField = "ImageGroupId" .DataTextField = "ImageGroup" .DataBind() .SelectedIndex = 0 End With End If End Sub
Add a handler in the script block for the Load event of the Page object:
Sub Page_Load(sender As Object, e As EventArgs) End Sub
In the Page_Load handler, add a call to your new method LoadGridData if it is not a PostBack:
If Not Page.IsPostBack Then LoadImageGroups() LoadGridData() End If
Add a handler in the script block for the SelectedIndexChanged of the cboImageGroups DropDownList.
Sub cboImageGroups_SelectedIndexChanged( _ sender As Object, e As EventArgs) End Sub
In the HTML, link the cboImageGroups DropDownList to the event handler by adding the following attribute:
OnSelectedIndexChanged="cboImageGroups_SelectedIndexChanged"
Modify the SelectedIndexChanged event handler so that it calls the LoadGridData method:
LoadGridData()
Compile your assembly by running BuildAll.bat again.
Start your Web browser and navigate to https://localhost/mypics/Default.aspx.
You should see the Image ID and Description fields for any images you uploaded earlier displayed in the DataGrid, and you should be able to select different categories for your images.
Adding Basic Pagination
The HTML that defined the layout for the DataGrid set some basic properties to support pagination, but you now to add some logic to actually implement pagination.
Add a handler for the PageIndexChanged event of the grdImages control in your page.
Sub grdImages_PageIndexChanged(sender As Object, _ e As DataGridPageChangedEventArgs) End Sub
In the HTML, link the grdImages DataGrid to the event handler by adding the following attribute:
OnPageIndexChanged="grdImages_PageIndexChanged"
In this handler, set the CurrentPageIndex of the grdImages control to the incoming NewPageIndex property of the DataGridPageChangedEventArgs parameter. Then call your LoadGridData method.
grdImages.CurrentPageIndex = e.NewPageIndex LoadGridData()
In your handler for SelectedIndexChanged of the image group DropDownList (cboImageGroups_SelectedIndexChanged) reset the CurrentPageIndex of the DataGrid to zero before the call to LoadGridData:
grdImages.CurrentPageIndex = 0
You can try running your application now to verify that pagination is working correctly (just add more than five images to the database to see it in action).
Adding Support for Jumping Pages
In addition to supporting browsing for images a page at-a-time, you are going to add support for jumping to a specific page of images within a particular image group.
Add the following procedure (Item 18) to the Code section of your Default.aspx page to populate the cboGridPages DropDownList with the list of available pages:
Private Sub LoadCboPages() Dim dv As DataView = CType(Me.grdImages.DataSource, DataView) Dim intRowCount As Integer = dv.Count Dim intPageSize As Integer = 5 Dim intRemainder As Integer = intRowCount Mod intPageSize Dim intPages As Integer = _ ((intRowCount - intRemainder) \ intPageSize) If intRemainder > 0 Then intPages += 1 End If Dim pages(intPages - 1) As String For i As Integer = 0 To intPages - 1 pages(i) = "Page " & (i + 1).ToString() Next With Me.cboGridPages .DataSource = pages .DataBind() End With End Sub
Place a call to LoadCboPages at the end of your Page_Load handler when it is not a PostBack. Your Page_Load handler should now look like this:
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) 'Put user code to initialize the page here If Not Page.IsPostBack Then LoadImageGroups() LoadGridData() LoadCboPages() End If End Sub
Place another call to LoadCboPages at the end of your cboImageGroups_SelectedIndexChanged handler. It should look like this:
Private Sub cboImageGroups_SelectedIndexChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs) grdImages.CurrentPageIndex = 0 LoadGridData() LoadCboPages() End Sub
Now add a handler for the SelectedIndexChanged event of the cboGridPages DropDownList:
Sub cboGridPages_SelectedIndexChanged(sender As Object, e As EventArgs) End Sub
In the HTML, link the cboGridPages DropDownList to the event handler by adding the following attribute:
OnSelectedIndexChanged="cboGridPages_SelectedIndexChanged"
In this handler set the CurrentPageIndex of the grdImages DataGrid to whatever page was chosen and re-bind the DataGrid as follows:
Dim strSelected As String = Me.cboGridPages.SelectedValue Me.grdImages.CurrentPageIndex = (Convert.ToInt32(strSelected.Substring(5)) - 1) Me.LoadGridData()
Save your work and then run your application now to verify that page jumping is working correctly.
Enabling the Thumbnail Display
In order to render both thumbnail and normal images back to the client, you are going to build a custom HttpHandler. The handler will service HTTP endpoints that map to ShowImage.axd (AXD is a pre-registered extension in IIS for ASP.NET). You will use query string parameters to determine which file to stream back as an image.
Add a new class to your application called StreamImage in the MyPics namespace in a file named StreamImage.vb.
Add the following Imports declarations to the top of file above the class declaration:
Imports System Imports System.Collections.Specialized Imports System.IO Imports System.Web Imports System.Web.SessionState
Your class needs to implement both the IHttpHandler and the IReadOnlySessionState interfaces. Add these two interface statements to your class definition. Your file should look like this:
Imports System Imports System.Collections.Specialized Imports System.IO Imports System.Web Imports System.Web.SessionState Namespace MyPics Public Class StreamImage Implements IHttpHandler Implements IReadOnlySessionState End Class End Namespace
Add the following property implementation to your class:
Public ReadOnly Property IsReusable() As Boolean _ Implements IHttpHandler.IsReusable Get Return True End Get End Property
Add the following helper method to your class to stream an image from a file to the Response buffer (Item 19):
Private Sub WriteImage(ByVal ctx As HttpContext, _ ByVal FileName As String) Dim strContentType As String = "image/JPEG" Dim ext As String = IO.Path.GetExtension(FileName) Select Case ext Case ".gif" strContentType = "image/GIF" End Select ctx.Response.ContentType = strContentType ctx.Response.WriteFile(FileName) End Sub
Note This implementation supports JPEG and GIF files. To support addition image types, you will need to extend this procedure with additional content type values.
Finally, add the following implementation of the ProcessRequest method of the IHttpHandler interface (Item 20):
Public Sub ProcessRequest(ByVal ctx As HttpContext) _ Implements IHttpHandler.ProcessRequest Dim strPath As String = ctx.Request.Params("Path") If Not strPath Is Nothing Then ' TODO -- Add Role Check End If If Not File.Exists(strPath) Then strPath = ctx.Server.MapPath(AppGlobals.fileNotFound) End If Me.WriteImage(ctx, strPath) End Sub
Save your work. In order to enable your handler for the ShowImage.axd endpoint, you need to let the ASP.NET runtime know about it. Open your web.config file. Locate the second-level <system.web> element, and just below that element, add the following XML (Item 21):
<httpHandlers> <add verb="GET" path="ShowImage.axd" type="MyPics.StreamImage, MyPics" /> </httpHandlers>
Now, back in your Default.aspx file, you are ready to add the logic to render the thumbnail images as part of the DataGrid's rendering.
Find the string "<asp:Image id="imgThumbnail"". Add an ImageUrl attribute to the Image control as shown below (Item 22):
ImageUrl='<%# GetImageUrl(Container.DataItem, True) %>'
The complete image tag should look as follows:
<asp:Image id="imgThumbnail" runat="server" ImageAlign="Middle" ImageUrl="<%# GetImageUrl(Container.DataItem, True) %>"></asp:Image>
Add the following method to support the data binding expression you added to the page (Item 23):
Protected Function GetImageUrl( _ ByVal dataItem As Object, ByVal isThumbnail As Boolean) _ As String Dim strQstring As String Dim strImageUrl As String If isThumbnail Then strQstring = _ String.Format("Path={0}&MinRole={1}", _ DataBinder.Eval(dataItem, "FullImageThumbPath"), _ DataBinder.Eval(dataItem, "MinRole")) strImageUrl = "ShowImage.axd?" & strQstring Else strQstring = String.Format("Path={0}&MinRole={1}", _ DataBinder.Eval(dataItem, "FullImagePath"), _ DataBinder.Eval(dataItem, "MinRole")) strImageUrl = "ShowImage.aspx?" & strQstring End If Return strImageUrl End Function
Compile your assembly by running BuildAll.bat again.
You should now be able to run your program and see thumbnail images displayed in the DataGrid rendering.
Enabling Full Size Image Display
Create a new file called ShowImage.aspx.
Add the following directive as the first line in the file:
<%@ Page Language="VB" Explicit="True" Strict="True" %>
Add the HTML in Item 24 from codevbsdk.txt to setup the page's layout after the script block.
After the @ Page directive and before the <HTML> tag, add the following import directives:
<%@ import Namespace="MyPics" %>
Next add a script block after the import directives (and before the <HTML> tag):
<script runat="server"> </script>
Add a handler in the script block for the Load event of the Page object:
Sub Page_Load(sender As Object, e As EventArgs) End Sub
Add the following code to the Page_Load handler (Item 25). This will set the image URL of the Image control, passing along any query string that was passed to it:
Dim strQstring As String = "" Dim idx As Integer = Request.RawUrl.IndexOf("?") If idx > 0 Then strQstring = Request.RawUrl.Substring(idx + 1) End If ' Pass along the query string imgFullImage.ImageUrl = "ShowImage.axd?" & strQstring
To enable the links on your Default.aspx page to show full-size images, open Default.aspx. In the HTML, locate the lnkDisplayImage hyperlink in the ItemTemplate of the first column of the grdImages DataGrid. Add a NavigateUrl attribute to the control using the following string (Item 26):
NavigateUrl='<%# GetImageUrl(Container.DataItem, False) %>'
The completed HTML should like the following:
<asp:HyperLink id="lnkDisplayImage" NavigateUrl='<%# GetImageUrl(Container.DataItem, False) %>' runat="server">Display Image</asp:HyperLink>
Save your work. Running your application and you should now be able to click on the Display Image hyperlink in the first column of the DataGrid to view the full-sized image.
Adding Security
In this section, you are going to add security to the site so that only logged in users can upload images. In addition, viewable images will be restricted based on the user's login id and role.
Building a Security Class
Add a new class to your project called WebSecurity, in the MyPics namespace in a file called WebSecurity.vb.
Add the following Imports declarations to the top of file above the class declaration:
Imports System Imports System.Security.Cryptography Imports System.Text Imports System.Web.Security
Now add the following private member (note that the last character in the string sha1 is the number one, not the letter l or i):
Private Const DefCryptoAlg As String = "sha1"
Your file should look like this:
Imports System Imports System.Security.Cryptography Imports System.Text Imports System.Web.Security Namespace MyPics Public Class WebSecurity Private Const DefCryptoAlg As String = "sha1" End Class End Namespace
Add the following method (Item 27) to the class. It will be used later to stored hashed and salted passwords in the database:
Public Shared Sub HashWithSalt( _ ByVal plaintext As String, ByRef salt As String, _ ByRef hash As String) Const SALT_BYTE_COUNT As Integer = 16 If salt = Nothing OrElse salt = String.Empty Then Dim saltBuf(SALT_BYTE_COUNT) As Byte Dim rng As RNGCryptoServiceProvider = New RNGCryptoServiceProvider rng.GetBytes(saltBuf) Dim sb As StringBuilder = New StringBuilder(saltBuf.Length) Dim i As Integer For i = 0 To saltBuf.Length - 1 sb.Append(String.Format("{0:X2}", saltBuf(i))) Next salt = sb.ToString() End If hash = FormsAuthentication.HashPasswordForStoringInConfigFile( _ salt & plaintext, DefCryptoAlg) End Sub
You also need to add two methods to support encryption of the query string. This first method is for encrypting (Item 28):
Public Shared Function Encrypt(ByVal plaintext As String) As String ' Although designed to encrypt time-stamped tickets, using ' FormsAuthentication.Encrypt is by far the simplest way to ' encrypt strings. It does incur a small amount of additional space ' to store two date-time values and the size of the ' FormsAuthenticationTicket. The other advantage of this technique ' is that the encryption key is auto-generated and stored as ' an LSA secret for you. ' Be aware that the key is server-specific, and if you need to scale ' the application to a web farm you should set the decryption key ' in machine.config on all machines in the farm so that ' cross-machine encryption/decryption works properly Dim ticket As New FormsAuthenticationTicket( _ 1, "", System.DateTime.Now, System.DateTime.Now, _ False, plaintext, "") Return FormsAuthentication.Encrypt(ticket) End Function
This second method is for decrypting (Item 29):
Public Shared Function Decrypt(ByVal ciphertext As String) As String Dim ticket As FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(ciphertext) Return ticket.UserData End Function
Save your work.
Augmenting the Data Access Layer
Next, you need to add a new method to your data access layer to interact with the database and validate users.
Open the SSDAL.vb class file and add the following method to validate a set of user credentials against data stored in the database (Item 30):
Public Shared Function ValidateUser( _ ByVal UserAlias As String, ByVal UserPassword As String, _ ByRef UserId As Integer, ByRef RoleId As Integer) As Boolean Dim intRetVal As Boolean = False Dim strHash As String = Nothing Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim conn As SqlConnection Dim cmd As SqlCommand Dim prm As SqlParameter conn = New SqlConnection(conString) cmd = New SqlCommand("ws_ValidateUser", conn) cmd.CommandType = CommandType.StoredProcedure prm = cmd.Parameters.Add("@UserAlias", SqlDbType.VarChar, 255) prm.Value = UserAlias prm = cmd.Parameters.Add("@UserId", SqlDbType.Int) prm.Direction = ParameterDirection.Output prm = cmd.Parameters.Add("@UserHash", SqlDbType.VarChar, 50) prm.Direction = ParameterDirection.Output prm = cmd.Parameters.Add("@UserSalt", SqlDbType.VarChar, 50) prm.Direction = ParameterDirection.Output prm = cmd.Parameters.Add("@RoleId", SqlDbType.Int) prm.Direction = ParameterDirection.Output Try conn.Open() Dim intQRetVal As Integer = cmd.ExecuteNonQuery() Dim strDBHash As String = _ cmd.Parameters("@UserHash").Value.ToString() Dim strDBSalt As String = _ cmd.Parameters("@UserSalt").Value.ToString() WebSecurity.HashWithSalt(UserPassword, strDBSalt, strHash) If strDBHash = strHash Then UserId = Convert.ToInt32(cmd.Parameters("@UserId").Value) RoleId = Convert.ToInt32(cmd.Parameters("@RoleId").Value) intRetVal = True Else UserId = -1 RoleId = -1 End If Return intRetVal Finally If Not conn Is Nothing Then conn.Dispose() End If End Try End Function
In addition, you want the application to be able to retrieve images filtered by group and role membership. Add the following method that calls the GetImagesByImageGroupId stored procedure (Item 31):
Public Shared Function GetImagesByImageGroupId( _ ByVal GroupId As Integer, ByVal MinRole As Integer) _ As DataTable Dim dt As DataTable Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim mcon As SqlConnection = New SqlConnection(conString) Dim mcmd As SqlCommand = _ New SqlCommand("GetImagesByImageGroupId", mcon) mcmd.CommandType = CommandType.StoredProcedure Dim prm As SqlParameter prm = New SqlParameter("@ImageGroupId", SqlDbType.Int) prm.Value = GroupId mcmd.Parameters.Add(prm) prm = New SqlParameter("@MinRoleId", SqlDbType.Int) prm.Value = MinRole mcmd.Parameters.Add(prm) Dim msda As SqlDataAdapter = New SqlDataAdapter(mcmd) Dim ds As DataSet = New DataSet msda.Fill(ds, "ImagesByImageGroupId") dt = ds.Tables(0) Return dt End Function
Save your work.
Compile your assembly by running BuildAll.bat again.
Enabling User Login
The next task is to allow users to login by providing an e-mail alias and password.
To begin, open your AppGlobals.vb class file and add the following additional constant definitions:
Public Const sessKeyUserId As String = "UserId" Public Const sessKeyRoleId As String = "RoleId" Public Const errMsgInvalidUser As String = _ "Invalid User Id or Password" Public Const errMsgCSS As String = "ErrorText" Public Const infoMsgAnonymous As String = "Anonymous"
Save your work and close the AppGlobals.vb file.
In Default.aspx, add a handler in the script block for the Click event of the Login Button on your form:
Sub btnLogin_Click(sender As Object, e As EventArgs) End Sub
In the HTML, link the Login Button to the event handler by adding the following attribute:
onclick="btnLogin_Click"
Add the following code to process the Click event (Item 32):
Dim intUserId As Integer = -1 Dim intRoleId As Integer = -1 If SSDAL.ValidateUser( _ Me.txtUserAlias.Text, Me.txtUserPassword.Text, _ intUserId, intRoleId) Then ' TODO -- Add Session Handling FormsAuthentication.SetAuthCookie(Me.txtUserAlias.Text, False) Session(AppGlobals.sessKeyUserId) = intUserId Session(AppGlobals.sessKeyRoleId) = intRoleId Response.Redirect("default.aspx") Else Me.lblUserId.CssClass = AppGlobals.errMsgCSS Me.lblUserId.Text = AppGlobals.errMsgInvalidUser End If
Add the following import directive:
<%@ import Namespace="System.Web.Security" %>
Add a handler in the script block for the Click event of the Logout Button on your form:
Sub btnLogout_Click(sender As Object, e As EventArgs) End Sub
In the HTML, link the Logout Button to the event handler by adding the following attribute:
onclick="btnLogout_Click"
Add the following code to process the Click event (Item 33):
If User.Identity.IsAuthenticated Then Session.Remove(AppGlobals.sessKeyUserId) Session.Remove(AppGlobals.sessKeyRoleId) ' TODO -- Add Session Handling FormsAuthentication.SignOut() Response.Redirect("Default.aspx") End If
Finally, you need to change the appearance of the page based on whether the user is logged in or not. Add the following method (Item 34):
Private Sub AdjustUI() Dim fUA As Boolean = User.Identity.IsAuthenticated If fUA Then Me.lblUserId.Text = User.Identity.Name Else Me.lblUserId.Text = AppGlobals.infoMsgAnonymous End If Me.lblUserId.CssClass = String.Empty Me.pnlLogin.Visible = (Not fUA) Me.pnlLogout.Visible = fUA End Sub
Place a call to the newly added AdjustUI method at the top of your Page_Load handler. The Page_Load handler should look like this:
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) AdjustUI() If Not Page.IsPostBack Then LoadImageGroups() LoadGridData() LoadCboPages() End If End Sub
In order for all of this to work, ASP.NET Forms Authentication must be enabled in the application's configuration file. Open web.config and add the following XML below the <system.web> element (Item 35):
<authentication mode="Forms" />
Now that authentication is enabled, you need to ensure that anonymous users cannot upload images. In web.config, add the following XML snippet just below the top-level <configuration> element (Item 36):
<location path="NewImage.aspx"> <system.web> <authorization> <deny users="?" /> <!-- deny anonymous users --> </authorization> </system.web> </location>
There's one last task to perform before you test. You need modify the code used to add new image metadata to the database. The current procedure defaults to the user id value of one. Open NewImage.aspx and locate the comment TODO -- Once security is enabled, provide the correct user id in the btnUpload_Click handler. In the call to SSDAL.AddImage, and change the 5th parameter from the hard-coded value of 1 to the following:
Convert.ToInt32(Session.Item(AppGlobals.sessKeyUserId))
Now save your work, compile and test. You should be able to login with the following credentials:
E-mail: admin@nowhere.com
Password: passwordYou also should try an invalid combination to verify you cannot login.
Note Naturally if you changed the Admin user id and/or password when you built the database, you will need to provide those values.
Restricting Access to Images Based on User Role Membership
The database for this Web application is designed to restrict what images are visible to a user based upon the user's role membership. In addition, it can allow anonymous users to view images (if any exist in the database).
To start, you will restrict the image groups that a particular user can assign to any uploaded files. Open the NewImage.aspx in and locate the LoadImageGroups method. Modify it to look like the following (Item 37):
Private Sub LoadImageGroups() Dim dv As DataView = New DataView(SSDAL.ImageGroups) ' Perform Data Binding If Not dv Is Nothing Then With Me.cboImageGroups If User.Identity.IsAuthenticated Then dv.RowFilter = "MinRoleId <= " & _ Session(AppGlobals.sessKeyRoleId).ToString() Else dv.RowFilter = "MinRoleId = 0" End If .DataSource = dv .DataValueField = "ImageGroupId" .DataTextField = "ImageGroup" .DataBind() End With End If End Sub
Similarly, users assigned to a particular role can only restrict viewing of images they upload based on roles with an equal or lower privilege than what they have. In NewImage.aspx, locate the LoadRoles method. Modify it to look like the following (Item 38):
Private Sub LoadRoles() Dim dv As DataView = SSDAL.UserRoles ' Perform Data Binding If Not dv Is Nothing Then With Me.cboMinRole dv.RowFilter = "RoleId <= " & _ Session(AppGlobals.sessKeyRoleId).ToString() .DataSource = dv .DataValueField = "RoleId" .DataTextField = "RoleName" .DataBind() End With End If End Sub
Save your work, close NewImage.aspx.
In Default.aspx, locate the LoadGridData routine. Instead of retrieving all of the image metadata with the AllImages property of the data access class, you will use the new GetImagesByImageGroupId method. This method only retrieves those images that meet the group membership and security criteria of the current user. Change LoadGridData as follows (Item 39):
Private Sub LoadGridData() Dim intMinRoleId As Integer = 0 If User.Identity.IsAuthenticated Then intMinRoleId = _ Convert.ToInt32(Session(AppGlobals.sessKeyRoleId)) End If Dim groupId As Integer = _ Integer.Parse(cboImageGroups.SelectedValue) Dim dv As DataView = _ New DataView(SSDAL.GetImagesByImageGroupId(groupId, intMinRoleId)) Me.grdImages.DataSource = dv Me.grdImages.DataBind() End Sub
Similarly, you want to only load those image groups that are viewable by the currently logged in user. Locate the LoadImageGroups method in Default.aspx and change it to look like the following code (Item 40):
Private Sub LoadImageGroups() Dim dv As DataView = New DataView(SSDAL.ImageGroups) ' Perform Data Binding If Not dv Is Nothing Then With Me.cboImageGroups If User.Identity.IsAuthenticated Then dv.RowFilter = "MinRoleId <= " & _ Session(AppGlobals.sessKeyRoleId).ToString() Else dv.RowFilter = "MinRoleId = 0" End If .DataSource = dv .DataValueField = "ImageGroupId" .DataTextField = "ImageGroup" .DataBind() .SelectedIndex = 0 End With End If End Sub
Save your work, and close Default.aspx.
Lastly, you need to modify the StreamImage.vb handler to check for authenticated users and their role membership. Open StreamImage.vb and locate the ProcessRequest method. Within the method, find the comment TODO -- Add Role Check. After the comment, add the following logic (Item 41):
Dim intMinRole As Integer = 0 Dim strMinRole As String = ctx.Request.Params("MinRole") If Not strMinRole Is Nothing Then intMinRole = Integer.Parse(strMinRole) End If Dim intUserRoleLevel As Integer = 0 If ctx.User.Identity.IsAuthenticated Then intUserRoleLevel = CType(ctx.Session(AppGlobals.sessKeyRoleId), Integer) End If If intUserRoleLevel < intMinRole Then strPath = ctx.Server.MapPath(AppGlobals.fileDenied) End If
Save your work, compile, and test. Try adding images that require a certain role to view, and then try accessing them from an account with a lower role membership. Start by adding images using the Admin credentials listed earlier. Then try using the Guest account that has been assigned the role of 'Co-worker'. You should be able to login with the following credentials:
E-mail: guest@nowhere.com
Password: nopasswordNote Naturally if you changed the Guest user id and/or password when you built the database, you will need to use those values.
Encrypting Query String Parameters
One last security issue is that requests made to the ShowImage HttpHandler have the path of the image on the server passed in clear text in the query string in addition to the user's role id. To see this in action, run the application before you perform this section and notice the value displayed in your browser's address bar whenever you view an image in full size mode.
To fix this, you are now going to encrypt the query string parameters for all requests made to the custom image handler.
First, you need add the logic to encrypt the query string. Open the Default.aspx and locate the GetImageUrl method and call WebSecurity.Encrypt with the strQstring variable before concatenating it to the rest of the image URL as follows (Item 42):
Protected Function GetImageUrl( _ ByVal dataItem As Object, ByVal isThumbnail As Boolean) _ As String Dim strQstring As String Dim strImageUrl As String If isThumbnail Then strQstring = String.Format("Path={0}&MinRole={1}", _ DataBinder.Eval(dataItem, "FullImageThumbPath"), _ DataBinder.Eval(dataItem, "MinRole")) strImageUrl = "ShowImage.axd?" & _ WebSecurity.Encrypt(strQstring) Else strQstring = String.Format("Path={0}&MinRole={1}", _ DataBinder.Eval(dataItem, "FullImagePath"), _ DataBinder.Eval(dataItem, "MinRole")) strImageUrl = "ShowImage.aspx?" & _ WebSecurity.Encrypt(strQstring) End If Return strImageUrl End Function
In addition, you need to modify the btnUpload_Click handler in NewImage.aspx to encrypt the query string so that you can view an image after it's been uploaded. Open NewImage.aspx and search for the string TODO -- Add Encryption. Modify the code the sets the hlinkViewImage hyperlink's NavigateUrl property to include a call to WebSecurity.Encrypt:
hlinkViewImage.NavigateUrl = _ String.Format("ShowImage.aspx?{0}", _ WebSecurity.Encrypt("Path=" & strServerPath & strFileNameOnly))
Save and close NewImage.aspx.
Next, you need to decrypt the query string when it arrives at the custom image handler. Open StreamImage.vb and add the following helper method to parse the newly encrypted query string (Item 43):
Private Function ParseQueryString( _ ByVal ctx As HttpContext) As NameValueCollection Dim values As NameValueCollection = New NameValueCollection Dim strQstring As String = String.Empty Dim idx As Integer = ctx.Request.RawUrl.IndexOf("?") If idx > 0 Then strQstring = ctx.Request.RawUrl.Substring(idx + 1) strQstring = WebSecurity.Decrypt(strQstring) Dim stringPairs As String() = strQstring.Split("&"c) For Each s As String In stringPairs Dim pair As String() = s.Split("="c) values(pair(0)) = pair(1) Next End If Return values End Function
Finally, you need to modify the ProcessRequest method of StreamImage.vb. Start by calling the new helper function at the beginning of the request processing:
Dim values As NameValueCollection = ParseQueryString(ctx)
Then replace all references to ctx.Request.Params with the new values variable. Your ProcessRequest method should look as follows (Item 44):
Public Sub ProcessRequest(ByVal ctx As HttpContext) _ Implements IHttpHandler.ProcessRequest Dim values As NameValueCollection = ParseQueryString(ctx) Dim strPath As String = values("Path") If Not strPath Is Nothing Then Dim intMinRole As Integer = 0 Dim strMinRole As String = values("MinRole") If Not strMinRole Is Nothing Then intMinRole = Integer.Parse(strMinRole) End If Dim intUserRoleLevel As Integer = 0 If ctx.User.Identity.IsAuthenticated Then intUserRoleLevel = CType(ctx.Session(AppGlobals.sessKeyRoleId), Integer) End If If intUserRoleLevel < intMinRole Then strPath = ctx.Server.MapPath(AppGlobals.fileDenied) End If End If If Not File.Exists(strPath) Then strPath = ctx.Server.MapPath(AppGlobals.fileNotFound) End If Me.WriteImage(ctx, strPath) End Sub
Save your work, compile and test. Verify that the query string passed to ShowImage.aspx is indeed encrypted, and that the image still displays correctly.
Tracking Sessions
In this section, you will add the ability to track user sessions in the database. This requires modifying the data access layer and some of the presentation layer code.
Adding Session Tracking to the Data Access Layer
First you need to add a new method to interact with the database and validate users. Open the SSDAL.vb class file, and add the following method to invoke the sm_SessionCreated stored procedure (Item 45):
Public Shared Sub SessionCreated(ByVal SID As String) Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim conn As SqlConnection Dim cmd As SqlCommand Try conn = New SqlConnection(conString) cmd = New SqlCommand("sm_SessionCreated", conn) cmd.CommandType = CommandType.StoredProcedure Dim prm As SqlParameter = _ cmd.Parameters.Add("@SessionIdAspNet", SqlDbType.VarChar, 24) prm.Value = SID prm = cmd.Parameters.Add("@SessionCreated", SqlDbType.DateTime) prm.Value = Date.Now conn.Open() Dim intRv As Integer = cmd.ExecuteNonQuery() Finally If Not conn Is Nothing Then conn.Dispose() End If End Try End Sub
Next, add the following method to invoke the sm_SessionEnded stored procedure (Item 46):
Public Shared Sub SessionEnded(ByVal SID As String) Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim conn As SqlConnection = Nothing Dim cmd As SqlCommand Try conn = New SqlConnection(conString) cmd = New SqlCommand("sm_SessionEnded", conn) cmd.CommandType = CommandType.StoredProcedure Dim prm As SqlParameter = _ cmd.Parameters.Add("@SessionIdAspNet", SqlDbType.VarChar, 24) prm.Value = SID prm = cmd.Parameters.Add("@SessionEnded", SqlDbType.DateTime) prm.Direction = ParameterDirection.Input prm.Value = Date.Now conn.Open() Dim intRv As Integer = cmd.ExecuteNonQuery() Finally If Not conn Is Nothing Then conn.Dispose() End If End Try End Sub
Finally, add the following method to invoke the sm_SessionUserAuthenticated stored procedure (Item 47):
Public Shared Sub SessionUserAuthenticated( _ ByVal SID As String, ByVal UserId As Integer) Dim conString As String conString = _ ConfigurationSettings.AppSettings("ConnectionString") Dim conn As SqlConnection = Nothing Dim cmd As SqlCommand Try conn = New SqlConnection(conString) cmd = New SqlCommand("sm_SessionUserAuthenticated", conn) cmd.CommandType = CommandType.StoredProcedure Dim prm As SqlParameter = _ cmd.Parameters.Add("@SessionIdAspNet", SqlDbType.VarChar, 24) prm.Direction = ParameterDirection.Input prm.Value = SID prm = cmd.Parameters.Add("@Authenticated", SqlDbType.DateTime) prm.Direction = ParameterDirection.Input prm.Value = Date.Now prm = cmd.Parameters.Add("@UserId", SqlDbType.Int) prm.Direction = ParameterDirection.Input prm.Value = UserId conn.Open() Dim intRv As Integer = cmd.ExecuteNonQuery() Finally If Not conn Is Nothing Then conn.Dispose() End If End Try End Sub
Save your work.
Adding Session Tracking Code
Now that the data access layer supports adding session data, you need to have the application use this code at the appropriate points as a user interacts with the application.
Open Default.aspx and in the btnLogin_Click handler, place a call to the new SSDAL.SessionUserAuthenticated method with the SessionID and the user id just retrieved right after the call to SSDAL.ValidateUser. Your handler should look like this (Item 48):
Private Sub btnLogin_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) Dim intUserId As Integer = -1 Dim intRoleId As Integer = -1 If SSDAL.ValidateUser( _ Me.txtUserAlias.Text, Me.txtUserPassword.Text, _ intUserId, intRoleId) Then SSDAL.SessionUserAuthenticated(Session.SessionID, intUserId) FormsAuthentication.SetAuthCookie(Me.txtUserAlias.Text, False) Session(AppGlobals.sessKeyUserId) = intUserId Session(AppGlobals.sessKeyRoleId) = intRoleId Response.Redirect("default.aspx") Else Me.lblUserId.CssClass = AppGlobals.errMsgCSS Me.lblUserId.Text = AppGlobals.errMsgInvalidUser End If End Sub
In the btnLogout_Click handler, add the following line of code after the comment TODO -- Add Session Handling and before the next line of code:
SSDAL.SessionEnded(Session.SessionID)
Create a new filed Global.asax.
Add the following top-level directives:
<%@ Application language="VB" %> <%@ import Namespace="MyPics" %>
Next add a script block after the import directive:
<script runat="server"> </script>
Add a Session_Start event handler as follows in the script block:
Sub Session_Start(Sender As Object, E As EventArgs) End Sub
In the Session_Start handler, add a call to your new SSDAL.SessionCreated method:
SSDAL.SessionCreated(Session.SessionID)
Add a Session_End event handler as follows:
Sub Session_End(Sender As Object, E As EventArgs) End Sub
In the Session_End handler and add a call to your new SSDAL.SessionEnded method:
SSDAL.SessionEnded(Session.SessionID)
Save your work, compile and test. To verify, run a query against the Sessions table in the database.
Error Handling
In this last section, you will add a handler for any unhandled exceptions and provide an error message to the user.
Create a new file called CustomError.aspx.
Add the following directive as the first line in the file:
<%@ Page Language="VB" Explicit="True" Strict="True" %>
Add the HTML in Item 49 from codevbsdk.txt to setup the page's layout after the script block.
After the @ Page directive and before the <HTML> tag, add the following import directive:
<%@ import Namespace="MyPics" %>
Next add a script block after the import directives (and before the <HTML> tag):
<script runat="server"> </script>
Add a handler in the script block for the Load event of the Page object:
Sub Page_Load(sender As Object, e As EventArgs) End Sub
Add the following code (Item 50) to the Page_Load handler:
Dim ex As Exception = Server.GetLastError() If Not ex Is Nothing Then lblError.Text = ex.Message End If
Open Global.asax and add an Application_Error event handler as follows in the script block:
Sub Application_Error(Sender As Object, E As EventArgs) End Sub
Add the following code to the Application_Error handler:
Server.Transfer("CustomError.aspx")
Save and close Global.asax.
Open web.config and under the second-level <system.web> element, add the following XML (Item 51):
<customErrors mode="RemoteOnly" />
Save your work, compile, and test. One way to get an unexpected error is to stop your MSDE instance and try and run the application.
Conclusion
ASP.NET 1.1 and MSDE make it easy to build a data-driven website. Making it secure and able to perform requires just a bit more effort. As a sample, this is far from being a complete application. Spend some time and think of ways to enhance the application to better suit your needs.
Appendix A: Changing File System Permissions
If you are running this walkthrough on Windows XP or earlier, the account you are looking for is ASP.NET worker account (aspnet_wp). If you are using Windows Server™ 2003, you will use the NETWORK SERVICE account.
- Start Windows Explorer.
- Navigate to the directory you want to modify. Typically for this walk through it will be C:\Inetpub\wwwroot\mypics\uploads.
- Right-click the directory and select the Properties command.
- Click the Security tab. If you do not see the appropriate account in the list, add it.
- Give the account Modify permissions (this will grant additional rights).
- Click OK.
Additional Links
For more information see the following:
Using MSDE 2000 in a Web Application
Visual Basic and the .NET Framework SDK Sample
Additional walkthroughs:
MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual Basic .NET and ASP.NET Web Matrix
MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual C# .NET and the .NET Framework SDK
MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual C# .NET and Visual Studio .NET 2003
MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual C# .NET and ASP.NET Web Matrix
Related Books
Database Design for Mere Mortals: A Hands-On Guide to Relational Database Design, Second Edition by Michael J. Hernandez, Addison Wesley Professional, 2003. ISBN: 0201752840
Essential ASP.NET with Examples in C# by Fritz Onion Addison Wesley Professional, 2003. ISBN: 0201760401
Essential ASP.NET with Examples in Visual Basic .NET by Fritz Onion, Addison Wesley Professional, 2003. ISBN: 0201760398
Microsoft ASP.NET Coding Strategies with the Microsoft ASP.NET Team by Matthew Gibbs and Rob Howard, Microsoft Press, 2003. ISBN: 073561900X