MSDE 2000 Walkthrough: Build a Data-Driven Website Using Visual C# .NET and the .NET Framework SDK

 

Brian A. Randell
MCW Technologies, LLC

March 2004

Applies to:
   Microsoft® .NET Framework SDK
   Microsoft® ASP.NET 1.1
   Microsoft® SQL Server™ 2000 Desktop Engine (MSDE)
   Microsoft® Visual C#® .NET

Summary: Create a data-driven website with MSDE and ASP.NET 1.1, with Visual C# .NET code. (43 printed pages)

Download the associated CSSDKSupport.exe walkthrough code sample.

Download the complete C# .NET 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. 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:

  • 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 files for instructions, which is available in the downloadable code sample listed at the start of this article.

  • The codecssdk.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.

    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 by taking advantage of this option will far outweigh the difficulties it adds as you write 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

  1. Use Windows Explorer or a command prompt to create a folder called mypics under C:\Inetpub\wwwoot.
  2. Start the Internet Information Services program.
  3. Navigate to the Default Web Site node for your local computer.
  4. Right-click and select New, Virtual Directory. Click Next on the wizard introduction page.
  5. In the Alias field type mypics and click Next.
  6. In the Directory field enter C:\Inetpub\wwwroot\mypics and click Next, Next again, and the Finish.
  7. 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:

  1. Create a new file named Styles.css.
  2. If you've not already done so, open the codecssdk.txt file.
  3. Copy the entire text of Item 1 from codecssdk.txt to the clipboard.
  4. In the Styles.css file, paste the text from the clipboard.
  5. 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. Put the following XML (which should be copied from codecssdk.txt, Item 2) into the file. There should be no line breaks in 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 Webserver. If this is not the case you will need to modify connection string, possibly adjusting the security settings also.

  3. Save your work and close the web.config file.

To create the data access class

  1. Create a file called SSDAL.cs

  2. Create public class named SSDAL in a namespace called MyPics.

  3. At the top of the file, add the following using directives:

    using System;
    using System.Configuration;
    using System.Data;
    using System.Data.SqlClient;
    
  4. Add the following static function to your new SSDAL class (Item 3 in codecssdk.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;
      }
    }
    
  5. 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;
        }
      }
    }
    
  6. 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 Webapplication.

Building the Upload Form

  1. Create a new file called NewImage.aspx.

  2. Add the following directive as the first line in the file:

    <%@ Page Language="C#" %>
    
  3. Add the HTML in Item 6 from codecssdk.txt to setup the page's layout after the script block.

  4. 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" %>
    
  5. Next add a script block after the import directives (and before the <HTML> tag):

    <script runat="server">
    
    </script>
    
  6. Next add the following two methods in the script block 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();
       }
    }
    
  7. Add a handler in the script block for the Load event of the Page object:

    void Page_Load(object sender, EventArgs e)
    {
    }
    
  8. Now, call the two methods you previously added from your Page_Load method as follows (Item 8):

    if (!Page.IsPostBack)
    {
       LoadImageGroups();
       LoadRoles();
    }
    
  9. Create a new file called AppGlobals.cs

  10. Create a public class named AppGlobals in a namespace called MyPics.

  11. 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"
    
  12. Back in NewImage.aspx, add a handler in the script block for the Click event of the Upload Now Button on your form:

    void btnUpload_Click(object sender, EventArgs e)
    {
    }
    
  13. In the HTML, link the Upload Now Button to the event handler by adding the following attribute:

    onclick="btnUpload_Click"
    
  14. Add the following logic (Item 10) to the handler. 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;
    }
    
  15. 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.

  16. Repeat the process and create a folder named bin.

  17. 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.

  18. 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.

  19. Start your Webbrowser and navigate to https://localhost/mypics/NewImage.aspx.

  20. 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. Create a new file called ImageUtil.cs

  2. Create public class named ImageUtil in a namespace called MyPics.

  3. Add the following using directives to the top of the file:

    using System;
    using System.Drawing;
    
  4. 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();
          }
       }
    }
    
  5. 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;
    }
    
  6. 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();
    }
    
  7. Add the following import directives:

    <%@ import Namespace="System.Drawing" %>
    <%@ import Namespace="System.Drawing.Imaging" %>
    
  8. Compile your assembly by running BuildAll.bat again.

  9. 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. Create a new file called Default.aspx.

  2. Add the following directive as the first line in the file:

    <%@ Page Language="C#" %>
    
  3. Add the HTML in Item 14 from codecssdk.txt to setup the page's layout after the script block.

  4. After the @ Page directive and before the <HTML> tag, add the following import directives:

    <%@ import Namespace="MyPics" %>
    <%@ import Namespace="System.Data" %>
    
  5. Next add a script block after the import directives (and before the <HTML> tag):

    <script runat="server">
    
    </script>
    
  6. 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];
          }
       }
    }
    
  7. In script block of your 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();
    }
    
  8. 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;
       }
    }
    
  9. Add a handler in the script block for the Load event of the Page object:

    void Page_Load(object sender, EventArgs e)
    {
    }
    
  10. In the Page_Load handler, add a call to your new method LoadGridData if it is not a PostBack:

    if (!Page.IsPostBack)
    {
      LoadImageGroups();
      LoadGridData();
    }
    
  11. Add a handler in the script block for the SelectedIndexChanged of the cboImageGroups DropDownList.

    void cboImageGroups_SelectedIndexChanged(object sender, EventArgs e)
    {
    }
    
  12. In the HTML, link the cboImageGroups DropDownList to the event handler by adding the following attribute:

    OnSelectedIndexChanged="cboImageGroups_SelectedIndexChanged"
    
  13. Modify the SelectedIndexChanged event handler so that it calls the LoadGridData method:

    LoadGridData();
    
  14. Compile your assembly by running BuildAll.bat again.

  15. Start your Webbrowser and navigate to https://localhost/mypics/Default.aspx.

  16. 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.

    void grdImages_PageIndexChanged(
       object sender, DataGridPageChangedEventArgs e)
    {
    }
    
  2. In the HTML, link the grdImages DataGrid to the event handler by adding the following attribute:

    OnPageIndexChanged="grdImages_PageIndexChanged"
    
  3. 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();
    
  4. 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;
    
  5. 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:

    void Page_Load(object sender, EventArgs e)
    {
       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:

    void cboImageGroups_SelectedIndexChanged(object sender, EventArgs e)
    {
       grdImages.CurrentPageIndex = 0;
       LoadGridData();
       LoadCboPages();
    }
    
  4. Now add a handler for the SelectedIndexChanged event of the cboGridPages DropDownList:

    void cboGridPages_SelectedIndexChanged(object sender, EventArgs e)
    {
    }
    
  5. In the HTML, link the cboGridPages DropDownList to the event handler by adding the following attribute:

    OnSelectedIndexChanged="cboGridPages_SelectedIndexChanged"
    
  6. 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();
    
  7. 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 in a file named StreamImage.cs.

  2. Add the following Imports declarations to the top of file above the class declaration:

    using System;
    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 file should look like this:

    using System;
    using System.Collections.Specialized;
    using System.IO;
    using System.Web;
    using System.Web.SessionState;
    
    namespace MyPics
    {
       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.

  9. 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) %>'
    
  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. 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. Create a new file called ShowImage.aspx.

  2. Add the following directive as the first line in the file:

    <%@ Page Language="C#" %>
    
  3. Add the HTML in Item 24 from codecssdk.txt to setup the page's layout after the script block.

  4. After the @ Page directive and before the <HTML> tag, add the following import directives:

    <%@ import Namespace="MyPics" %>
    
  5. Next add a script block after the import directives (and before the <HTML> tag):

    <script runat="server">
    
    </script>
    
  6. Add a handler in the script block for the Load event of the Page object:

    void Page_Load(object sender, EventArgs e)
    {
    }
    
  7. 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;
    
  8. 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) %>'
    
  9. The completed HTML should like the following:

    <asp:HyperLink id="lnkDisplayImage"
     NavigateUrl='<%# GetImageUrl(Container.DataItem, False) %>' runat="server">Display Image
    </asp:HyperLink>
    
  10. 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, in the MyPics namespace, in a file called WebSecurity.cs.

  2. Add the following using declarations to the top of file above the class declaration:

    using System;
    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 file should look like this:

    using System;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Security;
    
    namespace MyPics
    {
        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. In Default.aspx, add a handler in the script block for the Click event of the Login Button on your form:

    void btnLogin_Click(object sender, EventArgs e)
    {
    }
    
  4. In the HTML, link the Login Button to the event handler by adding the following attribute:

    onclick="btnLogin_Click"
    
  5. Add the following code to process the Click 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;
    }
    
  6. Add the following import directive:

    <%@ import Namespace="System.Web.Security" %>
    
  7. Add a handler in the script block for the Click event of the Logout Button on your form:

    void btnLogout_Click(object sender, EventArgs w)
    {
    }
    
  8. In the HTML, link the Logout Button to the event handler by adding the following attribute:

    onclick="btnLogout_Click"
    
  9. Add the following code to process the Click event (Item 33):

    if ( User.Identity.IsAuthenticated )
    {
       Session.Remove(AppGlobals.sessKeyUserId);
       Session.Remove(AppGlobals.sessKeyRoleId);
    
       // TODO -- Add Session Handling
       FormsAuthentication.SignOut();
    
       Response.Redirect("default.aspx");
    }
    
  10. 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;
    }
    
  11. 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:

    void Page_Load(object sender, EventArgs e)
    {
       AdjustUI();
       if (!Page.IsPostBack)
       {
         LoadImageGroups();
         LoadGridData();
         LoadCboPages();
       }
    }
    
  12. 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" />
    
  13. 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>
    
  14. 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[AppGlobals.sessKeyUserId])
    
  15. 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

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).

  1. 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):

    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. 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 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 login with the following credentials:
    E-mail: guest@nowhere.com
    Password: nopassword

    Note   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.

  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.cs. 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. 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 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. Create a new filed Global.asax.

  4. Add the following top-level directives:

    <%@ Application language="C#" %>
    <%@ import Namespace="MyPics" %>
    
  5. Next add a script block after the import directive:

    <script runat="server">
    
    </script>
    
  6. Add a Session_Start event handler as follows in the script block:

    void Session_Start(object sender, EventArgs e)
    {
    }
    
  7. In the Session_Start handler, add a call to your new SSDAL.SessionCreated method:

    SSDAL.SessionCreated(Session.SessionID);
    
  8. Add a Session_End event handler as follows:

    void Session_End(object sender, EventArgs e)
    {
    }
    
  9. In the Session_End handler and add a call to your new SSDAL.SessionEnded method:

    SSDAL.SessionEnded(Session.SessionID);
    
  10. 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. Create a new file called CustomError.aspx.

  2. Add the following directive as the first line in the file:

    <%@ Page Language="C#" %>
    
  3. Add the HTML in Item 49 from codecssdk.txt to setup the page's layout after the script block.

  4. After the @ Page directive and before the <HTML> tag, add the following import directive:

    <%@ import Namespace="MyPics" %>
    
  5. Next add a script block after the import directives (and before the <HTML> tag):

    <script runat="server">
    
    </script>
    
  6. Add a handler in the script block for the Load event of the Page object:

    void Page_Load(object sender, EventArgs e)
    {
    }
    
  7. Add the following code (Item 50) to the Page_Load handler:

    Exception ex = Server.GetLastError();
    if (ex != null)
    {
    lblError.Text = ex.Message;        
    }
    
  8. Open Global.asax and add an Application_Error event handler as follows in the script block:

    void Application_Error(object sender, EventArgs e) 
    {
    }
    
  9. Add the following code to the Application_Error handler:

    Server.Transfer("CustomError.aspx");
    
  10. Save and close Global.asax.

  11. Open web.config and under the second-level <system.web> element, add the following XML (Item 51):

    <customErrors mode="RemoteOnly" />
    
  12. 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.

  1. Start Windows Explorer.
  2. Navigate to the directory you want to modify. Typically for this walkthrough 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.

For more information see the following:

Using MSDE 2000 in a Web Application

C# .NET and the .NET Framework SDK Sample

Additional walkthroughs:

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

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

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