MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual C# .NET and ASP.NET Web Matrix

 

Brian A. Randell
MCW Technologies, LLC

March 2004

Applies to:
   Microsoft® ASP.NET 1.1
   Microsoft® ASP.NET Web Matrix 0.6 (Build 812)
   Microsoft® SQL Server™ 2000 Desktop Engine (MSDE 2000)
   Microsoft® Visual C#® .NET

Summary: Create a data-driven website with MSDE and ASP.NET 1.1, Microsoft Visual C# .NET code, and build it using ASP.NET Web Matrix 0.6 (Build 812). (42 printed pages)

Download the associated CSWMSupport.exe walkthrough code sample.

Download the complete C# .NET and Web Matrix 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. If the user is authenticated, data will be filtered by their role.

Prerequisites

To perform this walkthrough, the following software and components must be installed on the development computer:

  • ASP.NET Web Matrix 0.6 (Build 812)
  • 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 codecswm.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 sample code download file provided at the start of this article.

Getting Started

To get started, you will create a directory for your project.

To create an ASP.NET Web Application project

  1. Open Microsoft ASP.NET Web Matrix. You will see the New File dialog box appear. Cancel the dialog.
  2. Using the Workspace window, navigate to C:\Inetpub\wwwroot.
  3. Right-click on the wwwroot folder, and select Add New Folder.
  4. In the Create New Folder dialog, enter mypics.

Creating a Style Sheet

In order to give the application a consistent look and feel, you will use a cascading style sheet (CSS) file:

  1. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  2. Select (General) from the Templates pane.
  3. Select the Style Sheet template.
  4. Type Styles.css in the Filename box and click OK.
  5. If you've not already done so, open the codecswm.txt file.
  6. Copy the entire text of Item 1 from codecswm.txt to the clipboard.
  7. In the Styles.css file, highlight and replace the Body style by pasting the text on the clipboard.
  8. Save your work 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

  1. Create a configuration file named web.config for your application.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the Web.Config template and click OK.
  5. Locate the top-level <configuration> element, and just below that element, add the following XML (which can be copied from codecswm.txt, Item 2):
    <appSettings>
       <add key="ConnectionString" 
        value="Server=localhost;Database=Pics2Share;Trusted_Connection=True;Connection 
        Timeout=60;Pooling=True;Min Pool Size=0;Max Pool Size=5"/>
    </appSettings>
    
    Note   The configuration fragment 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.
  6. Save your work and close the web.config file.

To create the data access class

  1. Add a new class file to your project called SSDAL.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the Class template.
  5. Type SSDAL.cs in the Filename box, making sure C# is selected in the Language drop-down list.
  6. Type SSDAL in the Class field and MyPics in the Namespace box and click OK.
  7. At the top of the new class, add the following using directives:
    using System.Configuration;
    using System.Data;
    using System.Data.SqlClient;
    
  8. Add the following static function to your new SSDAL class (Item 3 in codecswm.txt):
    public static int AddImage(string ImageName, string ImageDesc,
      string ImagePath, string ImageThumb,
      int UserId, int MinRole, int ImageGroupId)
    {
      int retVal = -1;
      SqlConnection mcon=null;
      SqlCommand mcmd=null;
    
      string conString;
      conString = ConfigurationSettings.AppSettings["ConnectionString"];
    
      using (mcon = new SqlConnection(conString))
      {
        mcmd = new SqlCommand("AddImageMetaData", mcon);
        mcmd.CommandType = CommandType.StoredProcedure;
    
        SqlParameter prm;
        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 == null)
          prm.Value = DBNull.Value;
        else
          prm.Value = ImageThumb;
        
        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 = (int)mcmd.Parameters["RETURN_VALUE"].Value;
        return retVal;
      }
    }
    
  9. Add the following static property to your class (Item 4):
    public static DataTable ImageGroups
    {
      get 
      {
        DataTable dt;
        string conString = 
          ConfigurationSettings.AppSettings["ConnectionString"];
        using (SqlConnection conn = new SqlConnection(conString))
        {
          SqlCommand cmd = new SqlCommand("GetAllImageGroups", conn);
          cmd.CommandType = CommandType.StoredProcedure;
    
          SqlDataAdapter da = new SqlDataAdapter(cmd);
          DataSet ds = new DataSet();
          da.Fill(ds, "AllImageGroups");
          dt = ds.Tables[0];
          return dt;
        }
      }
    }
    
  10. Finally add the following static property to your class (Item 5):
    public static DataView UserRoles
    {
      get
      {
        DataView retVal = null;
        string conString = 
          ConfigurationSettings.AppSettings["ConnectionString"];
        using (SqlConnection conn = new SqlConnection(conString))
        {
          SqlCommand cmd = new SqlCommand("GetAllUserRoles", conn);
          cmd.CommandType = CommandType.StoredProcedure;
          SqlDataAdapter da = new SqlDataAdapter(cmd);
          DataSet ds = new DataSet("UserRoles");
          da.Fill(ds, "AllUserRoles");
          if (ds.Tables[0].Rows.Count > 0)
            retVal = ds.Tables[0].DefaultView;
          return retVal;
        }
      }
    }
    

Building the Image Upload Facility

In this section, you will build the facility to upload images to your Web application.

Building the Upload Form

  1. Add a new file to your project called NewImage.aspx.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the ASP.NET Page template.
  5. Type NewImage.aspx in the Filename box, making sure C# is selected in the Language drop-down list.
  6. Click the HTML tab for the active window, and insert the following HTML inside the <head> element:
    <TITLE>Add a New Image</TITLE>
    <LINK href="Styles.css" type="text/css" rel="stylesheet">
    
  7. In HTML view, replace the default <form></form> block with the HTML in Item 6 from codecswm.txt.
  8. Save your work and switch to All view. Between the @ Page directive and the <script> tag, add the following import directives:
    <%@ import Namespace="MyPics" %>
    <%@ import Namespace="System.IO" %>
    <%@ import Namespace="System.Data" %>
    
  9. Save your work and switch to Design view.
  10. Add a handler for the Click event of the Upload Now Button on your form by double-clicking it.
  11. Next add the following two methods to your class to populate the two DropDownList controls (Item 7).
    void LoadImageGroups()
    {
       DataView dv = new DataView(SSDAL.ImageGroups);
      
       // Perform Data Binding
       if (dv != null)
       {
          cboImageGroups.DataSource = dv;
          cboImageGroups.DataValueField = "ImageGroupId";
          cboImageGroups.DataTextField = "ImageGroup";
          cboImageGroups.DataBind();
       }
    }
    
    void LoadRoles()
    {
       DataView dv = SSDAL.UserRoles;
      
       // Perform Data Binding
       if (dv != null)
       {
          cboMinRole.DataSource = dv;
          cboMinRole.DataValueField = "RoleId";
          cboMinRole.DataTextField = "RoleName";
          cboMinRole.DataBind();
       }
    }
    
  12. Using the Properties window, select the Page object from the list of available items. Click on the Events button (the lightning bolt) and then double-click on the Load event to create a Page_Load handler.
  13. Now, call the two methods you previously added from your Page_Load method as follows (Item 8):
    if (!Page.IsPostBack)
    {
       LoadImageGroups();
       LoadRoles();
    }
    
  14. Add a new class to your project called AppGlobals. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  15. Select (General) from the Templates pane. Select the Class template.
  16. Type AppGlobals.cs in the Filename box, making sure C# is selected in the Language drop-down list.
  17. Type AppGlobals in the Class field and MyPics in the Namespace box and click OK.
  18. Add the following constant members to your AppGlobals class (Item 9):
    public const string pathUploads = "Uploads";
    
    public const string fileDenied = "images/denied.gif";
    public const string fileNotFound = "images/noimage.gif";
    
  19. Back in your NewImage.aspx file, locate the btnUpload_Click handler you generated earlier in Code view, and add the following logic (Item 10). This procedure will be enhanced a few more times before the application is complete.
    hlinkViewImage.Visible = false;
    string strUploadFileName = Upfile.PostedFile.FileName;
    string strFileNameOnly = Path.GetFileName(strUploadFileName);
    string strServerPath = Server.MapPath(AppGlobals.pathUploads);
    
    if ( !strServerPath.EndsWith("\\") )
       strServerPath += "\\";
    
    string strServerFileName = strServerPath + strFileNameOnly;
    
    try
    {
       // Save the file to disk
       Upfile.PostedFile.SaveAs(strServerFileName);
    
       // Generate the thumbnail
       string strThumbFile = "Thmb" + strFileNameOnly;
       string strFullThumbFile = strServerPath + strThumbFile;
    
       // TODO -- Generate Thumbnail
       
       // TODO -- Once security is enabled, 
       // provide the correct user id
    
       int intImageId = SSDAL.AddImage(strFileNameOnly, 
          txtImageDesc.Text,
          strServerPath, strThumbFile, 1,
          Convert.ToInt32(cboMinRole.SelectedValue),
          Convert.ToInt32(cboImageGroups.SelectedValue));
    
       if (intImageId > 0)
       {
          // TODO -- Add Encryption
          hlinkViewImage.NavigateUrl = 
             string.Format("ShowImage.aspx?{0}", 
             "Path=" + strServerPath + strFileNameOnly);
          hlinkViewImage.Visible = true;
       }
    }
    catch (Exception ex)
    {
       lblMsg.Text = ex.Message;
    }
    finally
    {
       if (lblMsg.Text.Length > 0)
          lblMsg.Visible = true;
    }
    
  20. Return to Design view and save your work.
  21. In the Workspace window, right-click on the mypics folder. Select Add New Folder from the context menu. Name the new folder Uploads.
    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.
  22. Repeat the process 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.
  23. Repeat the process one more time and create a folder named bin.
  24. 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.
  25. Back in Web Matrix, with the NewImage.aspx page active, press F5 to try and run the application so far. The first time you do this, Web Matrix will display the Start Web Application dialog. Verify that the correct physical directory path is displayed in the Application Directory box. This walkthrough assumes you are using IIS. Select the Use or create an IIS Virtual Root option and type mypics in the Application Name field and click Start.
  26. 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

  1. Add a new class file to your project called ImageUtil.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane. Select the Class template.
  4. Type ImageUtil.cs in the Filename box, making sure C# is selected in the Language drop-down list.
  5. Type ImageUtil in the Class field and MyPics in the Namespace box and click OK.
  6. Add the following using directive to the top of the file:
    using System.Drawing;
    
  7. Add a static function called GenerateThumb to the class as shown below (Item 11):
    public static Bitmap GenerateThumb(string FilePath)
    {
       // 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 int THUMBNAIL_HEIGHT = 120;
    
       Bitmap bmp = null;
       try
       {
          bmp = new Bitmap(FilePath);
          Decimal decRatio = ((Decimal)bmp.Width / bmp.Height);
          int intWidth = (int)(decRatio * THUMBNAIL_HEIGHT);
    
          Image.GetThumbnailImageAbort myCallback =
             new Image.GetThumbnailImageAbort(ThumbnailCallback);
       
          Image img = bmp.GetThumbnailImage(
             intWidth, THUMBNAIL_HEIGHT, myCallback, IntPtr.Zero);
    
          return (Bitmap)img;
       }
       catch (Exception)
       {
          return null;
       }
       finally
       {
          if (bmp != null)
          {
             bmp.Dispose();
          }
       }
    }
    
  8. Add one more function to satisfy the callback method required by the GetThumbnailImage method of the Framework's Bitmap class (Item 12):
    private static bool ThumbnailCallback()
    {
      // You have to supply this delegate, even though the thumbnail
      // retrieval doesn't actually use it. See the documentation 
      // for more information.
      return false;
    }
    
  9. 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:
    Bitmap bmp = null;
    try
    {
      if (!File.Exists(strFullThumbFile))
      {
        bmp = ImageUtil.GenerateThumb(strServerFileName);
        if (bmp != null)
          bmp.Save(strFullThumbFile, ImageFormat.Jpeg);
        else
          strFullThumbFile = null;
      }
    }
    catch (Exception)
    {
      strFullThumbFile = null;
    }
    finally
    {
      if (bmp != null)
        bmp.Dispose();
    }
    
  10. Save your work and switch to All view. Between the @ Page directive and the <script> tag, add the following import directives:
    <%@ import Namespace="System.Drawing" %>
    <%@ import Namespace="System.Drawing.Imaging" %>
    
  11. Compile your assembly by running BuildAll.bat again.
  12. 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.

  1. Add a new file to your project called Default.aspx.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the ASP.NET Page template.
  5. Type Default.aspx in the Filename box, making sure C# is selected in the Language drop-down list.
  6. Click the HTML tab for the active window, and insert the following HTML inside the <head> element:
    <TITLE>My Pictures</TITLE>
    <LINK href="Styles.css" type="text/css" rel="stylesheet">
    
  7. In HTML view, replace the default <form></form> block with the HTML in Item 14 from codecswm.txt.
  8. Save your work and switch to All view. Between the @ Page directive and the <script> tag, add the following import directives:
    <%@ import Namespace="MyPics" %>
    <%@ import Namespace="System.Data" %>
    
  9. Save your work and switch to Design view.
  10. 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.cs file and add the following read-only static property to the SSDAL class (Item 15):
    public static DataTable AllImages
    {
       get 
       {
          string conString = 
             ConfigurationSettings.AppSettings["ConnectionString"];
          using (SqlConnection conn = new SqlConnection(conString))
          {
             SqlCommand cmd = 
                new SqlCommand("GetAllImageData", conn);
             cmd.CommandType = CommandType.StoredProcedure;
             SqlDataAdapter da = new SqlDataAdapter(cmd);
             DataSet ds = new DataSet("Images");
             da.Fill(ds, "AllImages");
             return ds.Tables[0];
          }
       }
    }
    
  11. Now switch to Code view in Default.aspx page. Add a new method to your class to load the DataGrid (Item 16):
    private void LoadGridData()
    {
       DataView dv = new DataView(SSDAL.AllImages);
       dv.RowFilter = "ImageGroupId = " + cboImageGroups.SelectedValue;
    
       grdImages.DataSource = dv;
       grdImages.DataBind();
    }
    
  12. Add another method to populate the Image groups DropDownList (Item 17):
    private void LoadImageGroups()
    {
       DataView dv  = new DataView(SSDAL.ImageGroups);
    
       // Perform Data Binding
       if ( dv != null)
       {
          cboImageGroups.DataSource = dv;
          cboImageGroups.DataValueField = "ImageGroupId";
          cboImageGroups.DataTextField = "ImageGroup";
          cboImageGroups.DataBind();
          cboImageGroups.SelectedIndex = 0;
       }
    }
    
  13. Using the Properties window, select the Page object from the list of available items. Click on the Events button (the lightning bolt) and then double-click on the Load event to create a Page_Load handler.
  14. In the Page_Load handler, add a call to your new method LoadGridData if it is not a PostBack:
    if ( !Page.IsPostBack )
    {
       LoadImageGroups();
       LoadGridData();
    }
    
  15. Finally, switch to Design mode and double click on the cboImageGroups DropDownList to add a handler for the SelectedIndexChanged event. Modify the SelectedIndexChanged event handler so that it calls the LoadGridData method:
    LoadGridData();
    
  16. Compile your assembly by running BuildAll.bat again.
  17. Save your work and with Default.aspx active in Web Matrix, press F5. 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.

  1. Add a handler for the PageIndexChanged event of the grdImages control in your page. 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();
    
  2. 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;
    
  3. 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.

  1. 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 void LoadCboPages()
    {
       DataView dv = (DataView)grdImages.DataSource;
       int intRowCount =  dv.Count;
    
       int intPageSize = 5;
       int intRemainder = intRowCount % intPageSize;
       int intPages = ((intRowCount - intRemainder) / intPageSize);
    
       if ( intRemainder > 0 )
          intPages += 1;
    
       if (intPages == 0)
          intPages = 1; // deal with lower bound case
      
       string[] pages = new string[intPages];
      
       for (int i=0; i<intPages; i++)
          pages[i] = "Page " + (i+1).ToString();
      
       cboGridPages.DataSource = pages;
       cboGridPages.DataBind();
    }
    
  2. 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 void Page_Load(object sender, System.EventArgs e)
    {
       // Put user code to initialize the page here
       if ( !Page.IsPostBack )
       {
          LoadImageGroups();
          LoadGridData();
          LoadCboPages();
       }
    }
    
  3. Place another call to LoadCboPages at the end of your cboImageGroups_SelectedIndexChanged handler. It should look like this:
    private void cboImageGroups_SelectedIndexChanged
       (object sender, System.EventArgs e)
    {
       grdImages.CurrentPageIndex = 0;
       LoadGridData();
       LoadCboPages();
    }
    
  4. Now add a handler for the SelectedIndexChanged event of the cboGridPages control by double-clicking on the control in the page designer.
  5. In this handler set the CurrentPageIndex of the grdImages DataGrid to whatever page was chosen and re-bind the DataGrid as follows:
    string strSelected = cboGridPages.SelectedValue;
    grdImages.CurrentPageIndex = 
       (Convert.ToInt32(strSelected.Substring(5)) - 1);
    LoadGridData();
    
  6. 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.

  1. Add a new class to your application called StreamImage in the MyPics namespace.
  2. Add the following using declarations to the top of file above the class declaration:
    using System.Collections.Specialized;
    using System.IO;
    using System.Web;
    using System.Web.SessionState;
    
  3. Your class needs to implement both the IHttpHandler and the IReadOnlySessionState interfaces. Add these two interface statements to your class definition. Your class should look like this:
    using System;
    using System.Collections.Specialized;
    using System.IO;
    using System.Web;
    using System.Web.SessionState;
    
    public class StreamImage : IHttpHandler, IReadOnlySessionState
    {
    }
    
  4. Add the following property implementation to your class:
    public bool IsReusable  { get { return true; } }
    
  5. Add the following helper method to your class to stream an image from a file to the Response buffer (Item 19):
    private void WriteImage(HttpContext ctx, string FileName)
    {
       string strContentType = "image/JPEG";
       string ext = Path.GetExtension(FileName);
       if (ext == ".gif")
          strContentType = "image/GIF";
    
       ctx.Response.ContentType = strContentType;
       ctx.Response.WriteFile(FileName);
    }
    
    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.
  6. Finally, add the following implementation of the ProcessRequest method of the IHttpHandler interface (Item 20):
    public void ProcessRequest(HttpContext ctx)
    {
       string strPath = ctx.Request.Params["Path"];
       if (strPath != null)
       {
         // TODO -- Add role check
          
          if (!File.Exists(strPath))
          {
             strPath = ctx.Server.MapPath(AppGlobals.fileNotFound);
          }
    
          WriteImage(ctx, strPath);
       }
    }
    
  7. 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>
    
  8. 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. Open the Default.aspx file in HTML view.
  9. Search for the string "<asp:Image id="imgThumbnail"". This will locate the Image control that is within the ItemTemplate of the last column of the DataGrid. Add an ImageUrl attribute to the Image control as shown below (Item 22):
    ImageUrl='<%# GetImageUrl(Container.DataItem, true) %>'
    
  10. The complete image tag should look as follows:
    <asp:Image id="imgThumbnail"
     runat="server" ImageAlign="Middle" ImageUrl="<%# GetImageUrl(Container.DataItem, true) %>">
    </asp:Image>
    
  11. Save your changes and switch to Code view. Add the following method to support the data binding expression you added to the page (Item 23):
    protected string GetImageUrl(object dataItem, bool isThumbnail)
    {
      string imageUrl;
      string qstring;
    
      if (isThumbnail)
      {
        qstring = string.Format("Path={0}&MinRole={1}",
          DataBinder.Eval(dataItem, "FullImageThumbPath"),
          DataBinder.Eval(dataItem, "MinRole"));
        imageUrl = "ShowImage.axd?" + qstring;
      }
      else
      {
        qstring = string.Format("Path={0}&MinRole={1}",
          DataBinder.Eval(dataItem, "FullImagePath"),
          DataBinder.Eval(dataItem, "MinRole"));
        imageUrl = "ShowImage.aspx?" + qstring;
      }
    
      return imageUrl;
    }
    
  12. Compile your assembly by running BuildAll.bat again.
  13. You should now be able to run your program and see thumbnail images displayed in the DataGrid rendering.

Enabling Full Size Image Display

  1. Add a new file to your project called ShowImage.aspx.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the ASP.NET Page template.
  5. Type ShowImage.aspx in the Filename box, making sure C# is selected in the Language drop-down list.
  6. Click the HTML tab for the active window, and insert the following HTML inside the <head> element:
    <TITLE>View Full Size Image</TITLE>
    <LINK href="Styles.css" type="text/css" rel="stylesheet">
    
  7. In HTML view, replace the default <form></form> block with the HTML in Item 24 from codecswm.txt.
  8. Save your work and switch to All view. Between the @ Page directive and the <script> tag, add the following import directives:
    <%@ import Namespace="MyPics" %>
    
  9. Save your work and switch to Code view.
  10. Using the Properties window again, select the Page object from the list of available items. Click on the Events button (the lightning bolt) and then double-click on the Load event to create a Page_Load handler.
  11. 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:
    string strQstring = String.Empty;
    int idx = Request.RawUrl.IndexOf("?");
    if (idx > 0)
       strQstring = Request.RawUrl.Substring(idx + 1);
    
    // pass along the query string
    imgFullImage.ImageUrl = "ShowImage.axd?" + strQstring;
    
  12. To enable the links on your Default.aspx page to show full-size images, open Default.aspx in HTML mode and 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) %>'
    
  13. The completed HTML should like the following:
    <asp:HyperLink id="lnkDisplayImage"
     NavigateUrl='<%# GetImageUrl(Container.DataItem, false) %>' runat="server">Display Image
    </asp:HyperLink>
    
  14. 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

  1. Add a new class to your project called WebSecurity, put it in the MyPics namespace.
  2. Add the following using declarations to the top of file above the class declaration:
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Security;
    
  3. 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 string DefCryptoAlg = "sha1";
    
  4. Your class should look like this:
    namespace MyPics
    {
        using System;
        using System.Security.Cryptography;
        using System.Text;
        using System.Web.Security;
    
        public class WebSecurity
        {
            private const string DefCryptoAlg = "sha1";
            
        }
    }
    
  5. Add the following method (Item 27) to the class. It will be used later to stored hashed and salted passwords in the database:
    public static void HashWithSalt(
       string plaintext, ref string salt, out string hash)
    {
       const int SALT_BYTE_COUNT = 16;
       if (salt == null || salt == "")
       {
          byte[] saltBuf = new byte[SALT_BYTE_COUNT];
          RNGCryptoServiceProvider rng = 
             new RNGCryptoServiceProvider();
          rng.GetBytes(saltBuf);
          
          StringBuilder sb = 
             new StringBuilder(saltBuf.Length);
          for (int i=0; i<saltBuf.Length; i++)
          sb.Append(string.Format("{0:X2}", saltBuf[i]));
          salt = sb.ToString();
       }
    
       hash = FormsAuthentication.
          HashPasswordForStoringInConfigFile(
          salt+plaintext, DefCryptoAlg);
    }
    
  6. You also need to add two methods to support encryption of the query string. This first method is for encrypting (Item 28):
    public static string Encrypt(string plaintext)
    {
       /* 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 itself. 
        * 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 */
    
       FormsAuthenticationTicket ticket;
       ticket = new FormsAuthenticationTicket(1, "", DateTime.Now,
          DateTime.Now, false, plaintext, "");
    
       return FormsAuthentication.Encrypt(ticket);
    }
    
  7. This second method is for decrypting (Item 29):
    public static string Decrypt(string ciphertext)
    {
       FormsAuthenticationTicket ticket;
       ticket = FormsAuthentication.Decrypt(ciphertext);
       return ticket.UserData;
    }
    
  8. 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.

  1. Open the SSDAL.cs class file and add the following method to validate a set of user credentials against data stored in the database (Item 30):
    public static bool ValidateUser(
     string UserAlias, string UserPassword, ref int UserId, ref int RoleId)
    {
      bool intRetVal = false;
      string strHash = null;
      string conString = 
       ConfigurationSettings.AppSettings["ConnectionString"];
      SqlConnection conn;
      SqlCommand cmd;
      SqlParameter prm;
    
      using (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;
    
        conn.Open();
        int intQRetVal = cmd.ExecuteNonQuery();
        string strDBHash = cmd.Parameters["@UserHash"].Value.ToString();
        string strDBSalt = cmd.Parameters["@UserSalt"].Value.ToString();
        WebSecurity.HashWithSalt(UserPassword, ref strDBSalt, out strHash);
        if (strDBHash == strHash)
        {
          UserId = (int)cmd.Parameters["@UserId"].Value;
          RoleId = (int)cmd.Parameters["@RoleId"].Value;
          intRetVal = true;
        }
        else
        {
          UserId = -1;
          RoleId = -1;
        }
    
        return intRetVal;
      }
    }
    
  2. 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 static DataTable GetImagesByImageGroupId(
       int GroupId, int MinRole)
    {
       DataTable dt;
       string conString =
          ConfigurationSettings.AppSettings["ConnectionString"];
    
       using (SqlConnection conn = new SqlConnection(conString))
       {
          SqlCommand cmd = new SqlCommand(
             "GetImagesByImageGroupId", conn);
          cmd.CommandType = CommandType.StoredProcedure;
        
          SqlParameter prm = new SqlParameter(
             "@ImageGroupId", SqlDbType.Int);
          prm.Value = GroupId;
          cmd.Parameters.Add(prm);
    
          prm = new SqlParameter("@MinRoleId", SqlDbType.Int);
          prm.Value = MinRole;
          cmd.Parameters.Add(prm);
    
          SqlDataAdapter da = new SqlDataAdapter(cmd);
          DataSet ds = new DataSet();
          da.Fill(ds, "ImagesByImageGroupId");
          dt = ds.Tables[0];
    
          return dt;
       }
    }
    
  3. Save your work.
  4. 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.

  1. To begin, open your AppGlobals.cs class file and add the following additional constant definitions:
    public const string sessKeyUserId = "UserId";
    public const string sessKeyRoleId = "RoleId";
    
    public const string 
       errMsgInvalidUser = "Invalid User Id or Password";
    public const string errMsgCSS = "ErrorText";
    
    public const string infoMsgAnonymous = "Anonymous";
    
  2. Save your work and close the AppGlobals.cs file.
  3. Open Default.aspx in page design mode and add an event handler for the Login Button's Click event by double-clicking it, adding the following code to process the event (Item 32):
    int intUserId = -1;
    int intRoleId = -1;
    
    if (SSDAL.ValidateUser(txtUserAlias.Text, txtUserPassword.Text,
       ref intUserId, ref intRoleId))
    {
       // TODO -- Add Session Handling
       FormsAuthentication.SetAuthCookie(txtUserAlias.Text, false);
    
       Session[AppGlobals.sessKeyUserId] = intUserId;
       Session[AppGlobals.sessKeyRoleId] = intRoleId;
    
       Response.Redirect("default.aspx");
    }
    else
    {
       lblUserId.CssClass = AppGlobals.errMsgCSS;
       lblUserId.Text = AppGlobals.errMsgInvalidUser;
    }
    
  4. Add the following import directive:
    <%@ import Namespace="System.Web.Security" %>
    
  5. Return to Default.aspx in page design mode and add a handler for the Logout Button's Click event by double-clicking it and add the following code (Item 33):
    if ( User.Identity.IsAuthenticated )
    {
       Session.Remove(AppGlobals.sessKeyUserId);
       Session.Remove(AppGlobals.sessKeyRoleId);
    
       // TODO -- Add Session Handling
       FormsAuthentication.SignOut();
    
       Response.Redirect("default.aspx");
    }
    
  6. 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 void AdjustUI()
    {
       bool fUA = User.Identity.IsAuthenticated;
       if ( fUA )
          lblUserId.Text = User.Identity.Name;
       else
          lblUserId.Text = AppGlobals.infoMsgAnonymous;
    
       lblUserId.CssClass = String.Empty;
       pnlLogin.Visible = (!fUA);
       pnlLogout.Visible = fUA;
    }
    
  7. Place a call to this the newly added AdjustUI method at the top of your Page_Load handler. The Page_Load handler should look like this:
    private void Page_Load(object sender, System.EventArgs e)
    {
       AdjustUI();
    
       if ( !Page.IsPostBack )
       {
          LoadImageGroups();
          LoadGridData();
          LoadCboPages();
       }
    }
    
  8. 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" />
    
  9. 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>
    
  10. 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 in Code view 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))
    
  11. Now save your work, compile and test. You should be able to login with the following credentials:
    E-mail: admin@nowhere.com
    Password: password

    You 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

This database for this Web application is designed to restrict which 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).

  1. To start, you will restrict the image groups that a particular user can assign to any uploaded files. Open the NewImage.aspx in Code view and locate the LoadImageGroups method. Modify it to look like the following (Item 37):
    void LoadImageGroups()
    {
       DataView dv = new DataView(SSDAL.ImageGroups);
    
       // Perform Data Binding
       if (dv != null)
       {
          if (User.Identity.IsAuthenticated)
             dv.RowFilter = "MinRoleId <= " 
                + Session[AppGlobals.sessKeyRoleId].ToString();
          else
             dv.RowFilter = "MinRoleId = 0";
    
          cboImageGroups.DataSource = dv;
          cboImageGroups.DataValueField = "ImageGroupId";
          cboImageGroups.DataTextField = "ImageGroup";
          cboImageGroups.DataBind();
       }
    }
    
  2. 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):
    void LoadRoles()
    {
       DataView dv = SSDAL.UserRoles;
    
       // Perform Data Binding
       if (dv != null)
       {
          dv.RowFilter = "RoleId <= " + 
             Session[AppGlobals.sessKeyRoleId].ToString();
          cboMinRole.DataSource = dv;
          cboMinRole.DataValueField = "RoleId";
          cboMinRole.DataTextField = "RoleName";
          cboMinRole.DataBind();
       }
    }
    
  3. Save your work, close NewImage.aspx.
  4. Open Default.aspx in Code view and 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 void LoadGridData()
    {
       int intMinRoleId = 0;
       if ( User.Identity.IsAuthenticated )
          intMinRoleId =
             Convert.ToInt32(Session[AppGlobals.sessKeyRoleId]);
    
       int groupId = int.Parse(cboImageGroups.SelectedValue);
       DataView dv = new DataView(
          SSDAL.GetImagesByImageGroupId(groupId, intMinRoleId));
    
       grdImages.DataSource = dv;
       grdImages.DataBind();
    }
    
  5. 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 void LoadImageGroups()
    {
       DataView dv  = new DataView(SSDAL.ImageGroups);
      
       // Perform Data Binding
       if ( dv != null)
       {
          if ( User.Identity.IsAuthenticated )
             dv.RowFilter = "MinRoleId <= " + 
                Session[AppGlobals.sessKeyRoleId].ToString();
          else
             dv.RowFilter = "MinRoleId = 0";
    
          cboImageGroups.DataSource = dv;
          cboImageGroups.DataValueField = "ImageGroupId";
          cboImageGroups.DataTextField = "ImageGroup";
          cboImageGroups.DataBind();
          cboImageGroups.SelectedIndex = 0;
       }
    }
    
  6. Save your work, close Default.aspx.
  7. Lastly, you need to modify the StreamImage.cs handler to check for authenticated users and their role membership. Open StreamImage.cs and locate the ProcessRequest method. Within the method, find the comment TODO -- Add Role Check. After the comment, add the following logic (Item 41):
    int intMinRole = 0;
    string strMinRole = ctx.Request.Params["MinRole"];
    if (strMinRole != null)
       intMinRole = int.Parse(strMinRole);
      
    int intUserRoleLevel = 0;
    // use session
    if (ctx.User.Identity.IsAuthenticated)
       intUserRoleLevel = (int)ctx.Session[AppGlobals.sessKeyRoleId];
    
    if (intUserRoleLevel < intMinRole)
       strPath = ctx.Server.MapPath(AppGlobals.fileDenied);
    
  8. 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 log in with the following credentials:
    E-mail: guest@nowhere.com
    Password: nopassword
    NoteNaturally 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.

  1. 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 string GetImageUrl(object dataItem, bool isThumbnail)
    {
       string imageUrl;
       string qstring;
    
       if (isThumbnail)
       {
          qstring = string.Format("Path={0}&MinRole={1}",
             DataBinder.Eval(dataItem, "FullImageThumbPath"),
             DataBinder.Eval(dataItem, "MinRole"));
          imageUrl = "ShowImage.axd?" + WebSecurity.Encrypt(qstring);
       }
       else
       {
          qstring = string.Format("Path={0}&MinRole={1}",
             DataBinder.Eval(dataItem, "FullImagePath"),
             DataBinder.Eval(dataItem, "MinRole"));
          imageUrl = "ShowImage.aspx?" + WebSecurity.Encrypt(qstring);
       }
    
       return imageUrl;
    }
    
  2. 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));
    
  3. Save and close NewImage.aspx.
  4. Next, you need to decrypt the query string when it arrives at the custom image handler. Open StreamImage.cs and add the following helper method to parse the newly encrypted query string (Item 43):
    private NameValueCollection ParseQueryString(HttpContext ctx)
    {
       NameValueCollection values = new NameValueCollection();
       string qstring = string.Empty;
       int idx = ctx.Request.RawUrl.IndexOf("?");
       if (idx > 0)
       {
          qstring = ctx.Request.RawUrl.Substring(idx + 1);
          qstring = WebSecurity.Decrypt(qstring);
          string[] stringPairs = qstring.Split('&');
          foreach (string s in stringPairs)
          {
             string[] pair = s.Split('=');
             values[pair[0]] = pair[1];
          }
       }
       return values;
    }
    
  5. 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:
    NameValueCollection values = ParseQueryString(ctx);
    
  6. Then replace all references to ctx.Request.Params with the new values variable. Your ProcessRequest method should look as follows (Item 44):
    public void ProcessRequest(HttpContext ctx)
    {
       NameValueCollection values = ParseQueryString(ctx);
    
       string strPath = values["Path"];
       if (strPath != null)
       {
          // TODO - add role check
          int intMinRole = 0;
          string strMinRole = values["MinRole"]; 
          if (strMinRole != null)
             intMinRole = int.Parse(strMinRole);
    
          int intUserRoleLevel = 0;
          // use session
          if (ctx.User.Identity.IsAuthenticated)
             intUserRoleLevel = 
                (int)ctx.Session[AppGlobals.sessKeyRoleId];
    
          if (intUserRoleLevel < intMinRole)
             strPath = ctx.Server.MapPath(AppGlobals.fileDenied);
    
          if (!File.Exists(strPath))
             strPath = ctx.Server.MapPath(AppGlobals.fileNotFound);
    
          WriteImage(ctx, strPath);
       }
    }
    
  7. 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

  1. First you need to add a new method to interact with the database and validate users. Open the SSDAL.cs class file, and add the following method to invoke the sm_SessionCreated stored procedure (Item 45):
    public static void SessionCreated(string SID)
    {
       string conString = 
          ConfigurationSettings.AppSettings["ConnectionString"];
       SqlConnection conn = null;
       SqlCommand cmd;
    
       using (conn = new SqlConnection(conString))
       {
          cmd = new SqlCommand("sm_SessionCreated", conn);
          cmd.CommandType = CommandType.StoredProcedure;
          SqlParameter prm = 
             cmd.Parameters.Add("@SessionIdAspNet", 
             SqlDbType.VarChar, 24);
    
          prm.Value = SID;
          prm = cmd.Parameters.Add("@SessionCreated", 
             SqlDbType.DateTime);
    
          prm.Value = DateTime.Now;
    
          conn.Open();
          cmd.ExecuteNonQuery();
       }
    }
    
  2. Next, add the following method to invoke the sm_SessionEnded stored procedure (Item 46):
    public static void SessionEnded(string SID)
    {
       string conString = 
          ConfigurationSettings.AppSettings["ConnectionString"];
       SqlConnection conn = null;
       SqlCommand cmd;
    
       using (conn = new SqlConnection(conString))
       {
          cmd = new SqlCommand("sm_SessionEnded", conn);
          cmd.CommandType = CommandType.StoredProcedure;
          SqlParameter prm = 
             cmd.Parameters.Add("@SessionIdAspNet", 
             SqlDbType.VarChar, 24);
    
          prm.Value = SID;
          prm = cmd.Parameters.Add("@SessionEnded", 
             SqlDbType.DateTime);
          prm.Value = DateTime.Now;
    
          conn.Open();
          cmd.ExecuteNonQuery();
       }
    }
    
  3. Finally, add the following method to invoke the sm_SessionUserAuthenticated stored procedure (Item 47):
    public static void SessionUserAuthenticated(
       string SID, int UserId)
    {
       string conString = 
          ConfigurationSettings.AppSettings["ConnectionString"];
       SqlConnection conn = null;
       SqlCommand cmd;
    
       using (conn = new SqlConnection(conString))
       {
          cmd = new SqlCommand("sm_SessionUserAuthenticated", conn);
          cmd.CommandType = CommandType.StoredProcedure;
          SqlParameter prm = 
             cmd.Parameters.Add("@SessionIdAspNet", 
             SqlDbType.VarChar, 24);
    
          prm.Value = SID;
          prm = 
             cmd.Parameters.Add("@Authenticated", 
             SqlDbType.DateTime);
    
          prm.Value = DateTime.Now;
          prm = cmd.Parameters.Add("@UserId", 
             SqlDbType.Int);
    
          prm.Value = UserId;
    
          conn.Open();
          cmd.ExecuteNonQuery();
       }
    }
    
  4. 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.

  1. First, open the code-behind file Default.aspx.vb. 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 void btnLogin_Click(object sender, System.EventArgs e)
    {
       int intUserId = -1;
       int intRoleId = -1;
    
       if (SSDAL.ValidateUser(txtUserAlias.Text, txtUserPassword.Text,
          ref intUserId, ref intRoleId))
       {
          // TODO -- Add Session Handling
          SSDAL.SessionUserAuthenticated(Session.SessionID, intUserId);
          FormsAuthentication.SetAuthCookie(txtUserAlias.Text, false);
    
          Session[AppGlobals.sessKeyUserId] = intUserId;
          Session[AppGlobals.sessKeyRoleId] = intRoleId;
    
          Response.Redirect("default.aspx");
       }
       else
       {
          lblUserId.CssClass = AppGlobals.errMsgCSS;
          lblUserId.Text = AppGlobals.errMsgInvalidUser;
       }
    }
    
  2. 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);
    
  3. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  4. Select (General) from the Templates pane.
  5. Select the Global.asax template. Accept the default file name and make sure C# is selected in the Language drop-down list.
  6. At the top of the file, under the Application directive, add the following import statement:
    <%@ import Namespace="MyPics" %>
    
  7. Locate the Session_Start handler and add a call to your new SSDAL.SessionCreated method:
    SSDAL.SessionCreated(Session.SessionID);
    
  8. In that same file, locate the Session_End handler and add a call to your new SSDAL.SessionEnded method:
    SSDAL.SessionEnded(Session.SessionID);
    
  9. 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.

  1. Add a new file to your project called CustomError.aspx.
  2. Right-click on the mypics folder in the Workspace window and select Add New File from the menu.
  3. Select (General) from the Templates pane.
  4. Select the ASP.NET Page template.
  5. Type CustomError.aspx in the Filename box, making sure C# is selected in the Language drop-down list.
  6. Click the HTML tab for the active window, and insert the following HTML inside the <head> element:
    <TITLE>Unexpected Error</TITLE>
    <LINK href="Styles.css" type="text/css" rel="stylesheet">
    
  7. In HTML view, replace the default <form></form> block with the HTML in Item 49 from codecswm.txt.
  8. Save your work and switch to Code view.
  9. Using the Properties window, select the Page object from the list of available items. Click on the Events button (the lightning bolt) and then double-click on the Load event to create a Page_Load handler.
  10. Add the following code (Item 50) to the Page_Load handler:
    Exception ex = Server.GetLastError();
    if (ex != null)
    {
    lblError.Text = ex.Message;        
    }
    
  11. Open the file Global.asax in Code view and add the following code to the Application_Error handler:
    Server.Transfer("CustomError.aspx");
    
  12. Open web.config and under the second-level <system.web> element, add the following XML (Item 51):
    <customErrors mode="RemoteOnly" />
    
  13. 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, Web Matrix, and MSDE make it easy to build a data-driven Web site. 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.

  1. Start Windows Explorer.
  2. Navigate to the directory you want to modify. Typically for this walk through it will be C:\Inetpub\wwwroot\mypics\uploads.
  3. Right-click the directory and select the Properties command.
  4. Click the Security tab. If you do not see the appropriate account in the list, add it.
  5. Give the account Modify permissions (this will grant additional rights).
  6. Click OK.

Additional Links

For more information see the following:

Using MSDE 2000 in a Web Application

C# .NET and Web Matrix Sample

Additional walkthroughs:

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 Basic .NET and the .NET Framework SDK

MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual Basic .NET and Visual Studio .NET 2003

MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual Basic .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

Show: