An ASP.NET Application to View and Share Photos Online

By Greg Ennis

This article explains an ASP.NET application to view and share photos online. 







This application gives you some basic photo-sharing capability similar to Ofoto or Yahoo-photos. The advantage to rolling your own application is that you gain full control over the content and layout of your site (no popup ads!) and you can completely customize it to fit your needs.


This article describes the application, which is actually 2 separate apps - a back-end Windows Forms C# application that scans your directories and files to build a database, and an ASP.NET application that presents the photos and allows the user to view them and edit them (to provide a caption and description).

这篇文章描述这个应用程序,准确的说是两个分开的程序。一个是后台用来扫描你目录和文件用来建立数据库的Windows Formc#应用程序,一个是展示相片并允许用户浏览和编辑它们(加标题和描述)的ASP.net的前端浏览程序(。

Note that there is a C# Photo Album Viewer posted on CodeProject by Mark Nischalke, but that is Windows Forms only. I was looking for something enabled for use through a web browser.

要注意的是在CodeProject上已经有一个Mark Nischalke 发布的C#版本的相册浏览器,但是那个仅仅是运行在Windwos Form上。我正在找的是一些能够通过Web浏览器使用东西。



This article assume you have basic working knowledge of C#, Windows Forms, ASP.NET programming, and some SQL statement knowledge. You'll need to have either the full version of SQL Server, or you can get MSDE, the free version of SQL.

这篇文章假设你已经有基本的C#语言,Windows Form,以及一些Asp.net的编程知识。并且懂一些基本的SQL基本语句知道。同时你或者你完整版本的Sql Server数据库,或者是免费的Sql Server数据库MSDE

The application as it is currently built will scan for all *.jpg files in subdirectories of your pictures folder. Future enhancements may include recursively scanning all files in all subdirectories.


Using the code


Before reading the article you may want to get the code installed and running. To do this you should follow these steps:


  1. Download the Back-end Windows Forms app and unzip it somewhere on your hard drive. (下载后端的Windows Forms程序,解压后放在你硬盘的某个地方)

  2. Edit the application config file to specify your SQL server and the root folder containing your pictures. This file is called App.Config, but note that VS.NET will automatically copy and rename this file to NPdbman.exe.config when you build the project. (编辑程序的Config配置文件来指向你的Sql Server数据库和包含你图片的根目录。这个文件叫App.Config。但是要注意的是当你在新建这个工程的时候,VS.net的开发环境会自动拷贝并且重命名这个文件为你的应用程序名.Config.

  3. Run the NPdbman.exe application and select "Initialize" from the database menu. This will create the tables and constraints in a new SQL database called netpix. (运行Npdbman.exe程序并且从数据库菜单中选择”Initialize”,这将会在一个叫netpix的新数据库里创建表和结束。

  4. Select "Build" from the database menu. This will populate the tables with the information scanned from the folder specified in the configuration file. (选择数据库菜单中的编译(build)。这将会把一些从指定的配置文件目录中信息扫描移入数据库。

  5. Download the Front-end ASP.NET app and unzip it in the IIS wwwroot folder. Run the IIS configuration tool, and right-click on the netpix folder. Select 'Properties' and in the 'Application Settings' pane, select 'Create'. (下载前端的Asp.net程序,并把它解压在IISwwwroot文件夹中,然后运行IIS配置工具,然后在netpix文件夹上右击,选择属性,在应用程序设置面板中选择创建。

  6. Edit the web.config for the application and specify your SQL server connection. 编辑文件的web.config配置文件指定你的Sql Server数据库位置。

  7. You should now be able to browse to http://localhost/netpix/default.aspx and browse your photos! (现在你就可以在浏览器中输入地址http://localhost/netpix/default.aspx来浏览你的相册啦)

The database


It would have certainly been possible to build a simple application that did not use a database and simply scanned the folder and file information on the fly, but using a database will allow us to implement some advanced features which would have been awkward and difficult without the power of the RDBMS.




Here is a diagram of the database, which consists of just 2 small related tables:



The albums table is built from subdirectories of your Pictures folder. Each subdirectory maps to one album in the table. You can provide a description for an album, so that the user will see a name for the album which may or may not be the actual folder name in the file system.


The pics table is built from the *.jpg files found in each subdirectory (album). The pics table is related to the albums table by a foreign key constraint, because every picture must belong to an album. Most of the column names should be pretty self-explanatory except perhaps numviews. This column counts the number of times a user has viewed the full picture and is incremented by the ASP.NET code, every time that a user clicks on a picture. We will see this code shortly.


Stored procedures


There are just a couple stored procedures. The first one inserts a picture into the pics table (if it does not already exist there):


CREATE PROCEDURE CreatePic(@albumid int, @filename varchar(255), 
                           @width int, @height int, 
                           @imgdate datetime, @imgsize int) AS


IF NOT EXISTS(SELECT [id] FROM pics WHERE albumid=@albumid 
  AND [filename]=@filename)
  INSERT INTO pics (albumid, [filename], width, 
            height, imgdate, imgsize) 
         values (@albumid, @filename, @width, 
            @height, @imgdate, @imgsize);

The second is similar, but it operates on the albums table and it returns the identity value of the new record (or the existing one):


CREATE PROCEDURE CreateAlbum(@rootpath varchar(1024), 
                        @description varchar(255), @id int output) AS


SELECT @id = (SELECT [id] FROM albums WHERE rootpath=@rootpath);


  INSERT INTO albums (rootpath, [description]) 
                values (@rootpath, @description);

The back-end


The back end is a Windows Forms application that you run on the server to build the database by scanning your pictures folder. There are 2 basic functions: to reset (delete) everything in the database, and then to scan and build all the entries. This article won't discuss how you build a forms application or hook up menu entries, etc., because that is covered in depth elsewhere.

后端是一个运行在服务器上面的用来扫描相片目录来建立数据库的windows form的应用程序,有两个基本功能,重置(删除)数据库里面的所以数据,然后扫描并构建所有的个体,这篇文章不会讨论如何建立这个程序或者挂菜单等等,因为那会被涉及在其它深一些的文章。

Reset/ Initialize Code


Let's take a look at the reset/initialize code. First there is a generic routine which reads a .sql script file and executes it on the given connection:


private void ExecuteBatch(SqlConnection conn, string filename)


// Load the sql code to reset/build the database


    System.IO.StreamReader r = System.IO.File.OpenText(filename);

    string sqlCmd = r.ReadToEnd();



// Build & execute the command


    SqlCommand cmd = new SqlCommand(sqlCmd, conn);

    cmd.CommandType = CommandType.Text;



This is pretty basic SQL interaction. This code is executed for the scripts resetdb.sql, createalbum.sql and createpics.sql. These script file contains all the necessary SQL to drop and then CREATE TABLE and ALTER TABLE statements to setup the database schema and stored procedures. Any error that occurs here will be thrown from the application and handled by the generic popup handler.


Populating the database


The database is built by compiling the scanned information from the file system into a DataSet object which is then committed to the database. First we setup the objects we will use for creating the albums table:


// The dataset

DataSet ds = new DataSet();


// The command object calls the stored proc which either does the insert or

// returns the existing row id. In either case

// the output parameter id is then

// used to update our existing DataTable object with the actual id.

insertAlbumCmd = new SqlCommand("CreateAlbum", conn);

insertAlbumCmd.CommandType = CommandType.StoredProcedure;


          SqlDbType.VarChar, 1024, "rootpath");


          SqlDbType.VarChar, 256, "description");

insertAlbumCmd.Parameters.Add("@id", SqlDbType.Int, 0, "id");

insertAlbumCmd.Parameters["@id"].Direction = ParameterDirection.Output;

insertAlbumCmd.UpdatedRowSource = UpdateRowSource.OutputParameters;

We build the command which will invoke the stored procedure listed above. We tell ADO.NET that, after the insert, it should take the output parameter from the stored procedure and use this value (the identity value) to update the id column of the disconnected DataTable.


// The adapter only needs to perform an insert. (Select is for FillSchema)

albumsAdapter = new SqlDataAdapter("SELECT * FROM albums", conn);

albumsAdapter.InsertCommand = insertAlbumCmd;

albumsAdapter.FillSchema(ds, SchemaType.Mapped, "albums");

DataTable albums = ds.Tables["albums"];

Here we attach the command to a SqlDataAdapter object and then pull the schema from the database into our table.


// Need to seed negative values to prevent dups during the insert when SQL

// generated values conflict with ADO.NET generated values

DataColumn dc = albums.Columns["id"];

dc.AutoIncrementSeed = -1;

dc.AutoIncrementStep = -1;

This part is important because it avoids any duplicate keys being generated during the batch update. If the SQL server returns an identity value which already exists in the DataTable, an exception would be thrown. Using negative identity values prevents this from ever happening.


Finally we can get about doing the actual work:


string[] dirs = System.IO.Directory.GetDirectories(rootPath);


foreach (string dir in dirs)


    // Insert or update the album in the database

    string dirname = System.IO.Path.GetFileName(dir);


    // New row will populate the primary key for us

    DataRow dr = albums.NewRow();

    dr["rootpath"] = dir;

    dr["description"] = dirname;




// Commit the albums to the database

albumsAdapter.Update(ds, "albums");

The Update will insert all pending rows into the data store.


Populating the pictures table follows the same logic, so I won't repeat it here. For each *.jpg file found, a row is added to the DataTable and then the SqlDataUdapter Update method is invoked in order to perform the necessary inserts. The main difference are the columns required; for each image found, this method is called to collect the necessary data into the DataRow:


protected void GetImageInfo(string imgpath, DataRow dr)


    // Get data about this pic and populate 

    // the data row for the insert

    System.IO.FileStream fs = File.Open(imgpath, 

         FileMode.Open, FileAccess.Read, FileShare.Read);

    Bitmap img = new Bitmap(fs);

    dr["filename"] = System.IO.Path.GetFileName(imgpath);

    dr["imgsize"] = (int)fs.Length;

    dr["height"] = img.Height;

    dr["width"] = img.Width;

    dr["imgdate"] = File.GetCreationTime(imgpath);

    dr["numviews"] = 0;




Unfortunately this causes a major performance hit because each image must be loaded into memory in order to determine its dimensions. This is a one-time up-front operation, so this is an acceptable tradeoff.


The front-end


The front-end is the actual ASP.NET application which pulls our data out from the database and formats it nicely for the user.


The list of albums


The first thing the user sees is a list of albums, along with a little folder icon and a bit of information about the album itself (the number of pictures it contains). This is implemented with a DataList control. The control is defined here:


<asp:DataList id="dl" runat="server" 
          RepeatDirection="Horizontal" RepeatColumns="3">
  <table><tr><td><img src="folder.png"></td>
    <td><asp:HyperLink Runat="server" ID="hlItem" 
      NavigateUrl='<%# "viewalbum.aspx?id=" + 
             DataBinder.Eval(Container.DataItem, "id")%>' 
    <asp:Label Runat="server" ID="lbItem" 
               Text='<%# DataBinder.Eval(Container.DataItem, 
                             "piccount") + " pictures" %>'>

The important thing to note here is that each item is comprised of a folder bitmap, a HyperLink control, and a Label control. The Hyperlink has its text bound to the description of the album, and its URL bound to the viewalbum.aspx page. It passes the album ID to the viewalbum.aspx in the URL.


The code behind for this file is all of two lines:


    // Load the albums table and bind to the datalist

    dl.DataSource = npdata.GetAlbums();


The GetAlbums method is defined in a class named npdata. The npdata class contains static methods which encapsulate the data access adapters and commands to interface with the SQL database. The GetAlbums method does a basic select and fill and returns the DataSet. You may notice that the Label control references the piccount column, which does not exist in our schema. The piccount column is a calculated value which you can see in the query we use to bind to the list:


SqlDataAdpater adap = new SqlDataAdapter("SELECT *," +

                "(SELECT COUNT(*) FROM pics WHERE 

                AS piccount " +

                "FROM albums", conn);

So the piccount column is calculated by doing a sub-query on the pics table to determine how many pictures have a parent in the given album.


Viewing an album


When the user clicks on an album, the NavigateUrl property from the HyperLink control directs the browser to viewalbum.aspx and the album ID is passed along in the URL. This page generates a thumbnail for each image along with a basic description, and allows the user to click the image or edit the image properties. We once again utilize the DataList control for this functionality and it operates much the same way. The one point of note in this DataList is the actual URL for the thumbnail image:

当用户单击相册的时候,超连接控件的地址属性就会指向浏览器到viewalbum.aspx页面,并且相册的ID也随着地址传过去,这个页面对每个相片都会生成细的基本描述,也允许用户单击图片或者编辑图片属性。我们再一次利用datalist控件来实现这个功能,它的操作绝大部分是相同的,要注意的一点是在datalist 里面小图片是实际的URL地址

<img border="0" src='<%# "genimage.ashx?thumbnail=y&id=" 
               + DataBinder.Eval(Container.DataItem, "id") %>'>

We can't link directly to the .jpg file because the server is not directly sharing the images folder. So we link to a page called genimage.ashx which implements a sort of proxy that accepts the picture ID and streams the actual image data back to the client. It also accepts a thumbnail parameter, which indicates that the image should be sized down to 150x150. Note the .ashx extension; these are special files containing directives that you easily implement your own IHttpHandler-derived class. These classes give you a low-level interface to send data back to the client without all the overhead of creating and managing the lifecycle of a Page object. Our .ashx file contains only one line:


<%@ WebHandler Language="C#" Class="netpix.ImageGenerator" %>

This directs the client request for handling by the ImageGenerator class which is discussed in the next section.


Generating the pictures


The ImageGenerator class implements the IHttpHandler interface, which is a very simple and low-level interface for send raw streams of data back to the client. We are just dumping the bytes of image, so it's perfect for our needs. Since this class is key to the operation of the application, we will examine all of the code for this class:


public class ImageGenerator : IHttpHandler 


    public bool IsReusable 

        { get { return true; } } 


    public void ProcessRequest(HttpContext Context) 


        // Get the image filename and album root path from the database

        int numviews;

        int picid = Convert.ToInt32(Context.Request["id"]);

        string imgpath = npdata.GetPathToPicture(picid, out numviews);


        // Writing an image to output stream

        Context.Response.ContentType = "image/jpg";

Here we retrieve the picture ID from the URL and invoke the GetPathToPicture method which wraps a SQL join statement that returns the full local path to the image, as well as the number of times the image has been viewed on the client. Then we set the content type to jpg because we are impersonating a jpg file.

这儿,我们重新从URL中得到 像片的ID,然后调用封装了SQK连接语句的GetPathToPicture方法来返回图像完整的本地路径,和已经在客户端被浏览的次数。然后我们设置内容类型为 jpg,因为我们正在格式化一个jpg文件。

    // 'thumbnail' means we are requesting a thumbnail

    if (Context.Request["thumbnail"] != null)


        // Need to load the image, resize it, and stream to the client.

        // Calculate the scale so as not to stretch or distort the image.

        Bitmap bmp = new Bitmap(imgpath);

        float scale = 
   / System.Math.Max(bmp.Height, bmp.Width);

        System.Drawing.Image thumb = bmp.GetThumbnailImage(

            (int)(bmp.Width * scale), (int)(bmp.Height * scale), 

            null, System.IntPtr.Zero);






In the case where the Request URL contains the thumbnail parameter, we first load the image file from disk and call GetThumbnailImage to scale it down. We scale it down by a constant factor to maintain the aspect ratio so as not to distort the image. We then save the resized image directly to the Response object's output stream. This puts a pretty heavy stress on the server CPU when a large number of thumbnails are requested (I will discuss this in the 'Future Items' section).

在请求URL里包含了thumbnuil参数,我们首先从磁盘装载图片文件,然后调用GetThumbnailImage来缩放文件,我们是通过一个常数因子来维护它的外 观比率,从从而不至于使用图像失真,然后我们保存这个引人缩小了的图像到Response对象的输出流里,当大量的thumbnails被客户端请求的时候,就会给服务器的CUP增加一些压力。(我将会在Future Items’ Section里面讨论这一点。



        // Stream directly from the file

        System.IO.FileStream fs = File.Open(imgpath, 

            FileMode.Open, FileAccess.Read, FileShare.Read);


        // Copy it out in chunks

        const int byteLength = 16384;

        byte[] bytes = new byte[byteLength];

        while( fs.Read(bytes, 0, byteLength ) != 0 )






        // Now increment the view counter in the database

        npdata.SetNumViews(picid, numviews+1);



In this case, we are interested in streaming the image directly from the file contents. The current implementation reads the file contents in chunks and sends them to the response's output stream. We also need to increment the picture's view count in the database because the full-size image has been requested. The SetNumViews method just issues an SQL UPDATE statement to the pics table to set the numviews column for the given picture.


Viewing and editing a Picture


From the album view, the user can either view an image or edit the image information. There really isn't much of interest happening in viewpic.aspx or editpic.aspx. The user can supply title and description information in the editor, which will then be used by the viewer. The viewer will show the title for a picture if available, otherwise it will default to the filename. This is accomplished in the SQL statement:


    // The command to select a specific picture data

    getpicinfo = new SqlCommand("SELECT ISNULL(title, filename) 

                       AS returntitle, " +

                       "ISNULL([description],'') AS returndesc "+

                       "FROM pics WHERE", conn);

    getpicinfo.Parameters.Add("@picid", SqlDbType.Int);

Our schema design dictates that we will use the DB Null value to indicate no custom title is present. When this happens, we use the image filename as the text for the picture's caption.



  • 0
  • 0
    觉得还不错? 一键收藏
  • 打赏
  • 0


  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助




当前余额3.43前往充值 >
领取后你会自动成为博主和红包主的粉丝 规则




¥2 ¥4 ¥6 ¥10 ¥20
余额支付 (余额:-- )



钱包余额 0