一个 ASP.NET 应用程序,用于在线查看和共享照片






4.67/5 (47投票s)
2003年7月27日
11分钟阅读

418710

13266
本文介绍了一个 ASP.NET 应用程序,用于在线查看和共享照片。

引言
此应用程序提供了一些基本的照片共享功能,类似于 Ofoto 或 Yahoo-photos。自己构建应用程序的优势在于,您可以完全控制您网站的内容和布局(没有弹出广告!),并且可以根据您的需求进行完全自定义。
本文描述了该应用程序,它实际上是两个独立的应用程序 - 一个后端 Windows Forms C# 应用程序,用于扫描目录和文件以构建数据库;另一个 ASP.NET 应用程序,用于展示照片并允许用户查看和编辑它们(提供标题和描述)。
请注意,CodeProject 上有一个由 Mark Nischalke 发布的 C# 照片相册查看器,但它仅为 Windows Forms。我当时正在寻找可以通过 Web 浏览器使用的东西。
背景
本文假定您具备 C#、Windows Forms、ASP.NET 编程以及一些 SQL 语句的基础知识。您需要拥有完整版的 SQL Server,或者可以获取 MSDE,这是 SQL 的免费版本。
目前构建的应用程序将扫描您图片文件夹的子目录中所有 *.jpg 文件。未来的增强功能可能包括递归扫描所有子目录中的所有文件。
使用代码
在阅读本文之前,您可能希望安装并运行代码。为此,您应该按照以下步骤操作
- 下载 后端 Windows Forms 应用 并将其解压缩到硬盘上的某个位置。
- 编辑应用程序的配置文件,以指定您的 SQL 服务器和包含图片的根文件夹。该文件名为 App.Config,但请注意,VS.NET 在构建项目时会自动复制并重命名此文件为 NPdbman.exe.config。
- 运行 NPdbman.exe 应用程序,并从数据库菜单中选择“Initialize”。这将创建一个名为 netpix 的新 SQL 数据库,并创建其中的表和约束。
- 从数据库菜单中选择“Build”。这将使用从配置文件中指定的文件夹扫描的信息填充表。
- 下载 前端 ASP.NET 应用 并将其解压缩到 IIS 的 wwwroot 文件夹中。运行 IIS 配置工具,右键单击 netpix 文件夹。选择“Properties”,然后在“Application Settings”窗格中,选择“Create”。
- 编辑应用程序的 web.config 文件,并指定您的 SQL 服务器连接。
- 现在您应该可以浏览 https:///netpix/default.aspx 来浏览您的照片了!
数据库
当然,完全有可能构建一个不使用数据库、仅动态扫描文件夹和文件信息的简单应用程序,但使用数据库将使我们能够实现一些高级功能,这些功能在没有 RDBMS 的强大功能支持下会非常笨拙和困难。
架构
以下是数据库的图示,它仅包含 2 个小型的相关表。

albums 表是根据您 Pictures 文件夹的子目录构建的。每个子目录对应表中的一个相册。您可以为相册提供一个 description,这样用户看到的相册名称可能与文件系统中的实际文件夹名称相同,也可能不同。
pics 表是根据每个子目录(相册)中找到的 *.jpg 文件构建的。pics 表通过外键约束与 albums 表相关联,因为每张图片都必须属于一个相册。大多数列名都应该不言自明,除了 numviews 之外。此列计算用户查看完整图片的总次数,并且每当用户点击一张图片时,ASP.NET 代码就会将其递增。我们稍后会看到这段代码。
存储过程
只有几个存储过程。第一个是将一张图片插入到 pics 表(如果它尚不存在)。
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);
第二个是类似的,但它操作 albums 表,并返回新记录的标识值(或现有记录)。
CREATE PROCEDURE CreateAlbum(@rootpath varchar(1024), 
                        @description varchar(255), @id int output) AS
SELECT @id = (SELECT [id] FROM albums WHERE rootpath=@rootpath);
IF @id IS NULL
BEGIN
  INSERT INTO albums (rootpath, [description]) 
                values (@rootpath, @description);
  SET @id = SCOPE_IDENTITY();
END
后端
后端是一个 Windows Forms 应用程序,您在服务器上运行它来扫描您的 pictures 文件夹以构建数据库。它有两个基本功能:重置(删除)数据库中的所有内容,然后扫描并构建所有条目。本文不会讨论如何构建表单应用程序或连接菜单项等,因为这些内容在其他地方有详细介绍。
重置/初始化代码
让我们看一下重置/初始化代码。首先有一个通用例程,它读取一个 .sql 脚本文件并将其执行到给定的连接上。
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();
    r.Close();
    // Build & execute the command
    SqlCommand cmd = new SqlCommand(sqlCmd, conn);
    cmd.CommandType = CommandType.Text;
    cmd.ExecuteNonQuery();
}
这是非常基础的 SQL 交互。这段代码是为脚本 resetdb.sql、createalbum.sql 和 createpics.sql 执行的。这些脚本文件包含了所有必需的 SQL 语句,用于删除然后 CREATE TABLE 和 ALTER TABLE 语句来设置数据库架构和存储过程。任何在此处发生的错误都将从应用程序中抛出,并由通用的弹出处理程序处理。
填充数据库
数据库是通过将文件系统扫描的信息编译成一个 DataSet 对象来构建的,然后将其提交到数据库。首先,我们设置将用于创建 albums 表的对象。
// 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;
insertAlbumCmd.Parameters.Add("@rootpath", 
          SqlDbType.VarChar, 1024, "rootpath");
insertAlbumCmd.Parameters.Add("@description", 
          SqlDbType.VarChar, 256, "description");
insertAlbumCmd.Parameters.Add("@id", SqlDbType.Int, 0, "id");
insertAlbumCmd.Parameters["@id"].Direction = ParameterDirection.Output;
insertAlbumCmd.UpdatedRowSource = UpdateRowSource.OutputParameters;
我们构建将调用上述存储过程的命令。我们告诉 ADO.NET,在 insert 之后,它应该获取存储过程的输出参数,并使用此值(标识值)来更新断开连接的 DataTable 的 id 列。
// 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"];
在这里,我们将命令附加到 SqlDataAdapter 对象,然后从数据库中拉取模式到我们的表中。
// 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;
这一步很重要,因为它避免了在批量更新过程中生成任何重复键。如果 SQL 服务器返回的标识值已存在于 DataTable 中,则会引发异常。使用负标识值可以防止这种情况发生。
最后,我们可以开始进行实际工作了。
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;
    albums.Rows.Add(dr);
}
// Commit the albums to the database
albumsAdapter.Update(ds, "albums");
Update 将把所有待处理的行插入到数据存储中。
填充图片表遵循相同的逻辑,因此我在此不再重复。对于找到的每个 *.jpg 文件,都会向 DataTable 添加一行,然后调用 SqlDataUdapter 的 Update 方法来执行必要的插入。主要区别在于所需的列;对于找到的每个图像,都会调用此方法来收集必要的数据到 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;
    img.Dispose();
    fs.Close();
}
不幸的是,这会导致性能下降,因为必须将每个图像加载到内存中才能确定其尺寸。这是一个一次性的预处理操作,因此可以接受。
前端
前端是实际的 ASP.NET 应用程序,它从数据库中提取我们的数据并将其很好地格式化给用户。
相册列表

用户首先看到的是相册列表,以及一个小文件夹图标和关于该相册的一些信息(包含的图片数量)。这是通过 DataList 控件实现的。控件在此定义。
<asp:DataList id="dl" runat="server" 
          RepeatDirection="Horizontal" RepeatColumns="3">
  <ItemTemplate>
  <table><tr><td><img src="folder.png"></td>
    <td><asp:HyperLink Runat="server" ID="hlItem" 
      NavigateUrl='<%# "viewalbum.aspx?id=" + 
             DataBinder.Eval(Container.DataItem, "id")%>' 
      Text='<%#DataBinder.Eval(Container.DataItem, 
             "description")%>'>
    </asp:HyperLink><br>
    <asp:Label Runat="server" ID="lbItem" 
               Text='<%# DataBinder.Eval(Container.DataItem, 
                             "piccount") + " pictures" %>'>
    </asp:Label></td>
  </tr></table>
  </ItemTemplate>
</asp:DataList>
这里需要注意的重要一点是,每个项目都包含一个文件夹位图、一个 HyperLink 控件和一个 Label 控件。Hyperlink 的文本绑定到相册的描述,其 URL 绑定到 viewalbum.aspx 页面。它通过 URL 将相册 ID 传递给 viewalbum.aspx。
此文件的代码隐藏只有两行。
    // Load the albums table and bind to the datalist
    dl.DataSource = npdata.GetAlbums();
    dl.DataBind();
GetAlbums 方法定义在一个名为 npdata 的类中。npdata 类包含静态方法,这些方法封装了数据访问适配器和命令,用于与 SQL 数据库进行交互。GetAlbums 方法执行基本的 select 和 fill 操作,并返回 DataSet。您可能会注意到 Label 控件引用了 piccount 列,该列在我们的架构中并不存在。piccount 列是一个计算值,您可以在我们用于绑定到列表的查询中看到。
SqlDataAdpater adap = new SqlDataAdapter("SELECT *," +
                "(SELECT COUNT(*) FROM pics WHERE pics.albumid=albums.id) 
                AS piccount " +
                "FROM albums", conn);
因此,piccount 列是通过对 pics 表进行子查询来计算的,以确定有多少图片属于给定相册。
查看相册
当用户点击一个相册时,HyperLink 控件的 NavigateUrl 属性会将浏览器定向到 viewalbum.aspx,并将相册 ID 传递到 URL 中。此页面为每张图片生成缩略图和基本描述,并允许用户点击图片或编辑图片属性。我们再次为此功能使用 DataList 控件,其工作方式基本相同。此 DataList 中唯一值得注意的点是缩略图的实际 URL。
<img border="0" src='<%# "genimage.ashx?thumbnail=y&id=" 
               + DataBinder.Eval(Container.DataItem, "id") %>'>
我们不能直接链接到 .jpg 文件,因为服务器并未直接共享图片文件夹。因此,我们链接到一个名为 genimage.ashx 的页面,该页面实现了一种代理,它接受图片 ID 并将实际图像数据流式传输回客户端。它还接受一个缩略图参数,表示图像应缩小到 150x150。注意 .ashx 扩展名;这些是特殊文件,包含指令,让您可以轻松实现自己的 IHttpHandler 派生类。这些类提供了一个低级接口,用于将数据发送回客户端,而无需创建和管理 Page 对象生命周期的所有开销。我们的 .ashx 文件只有一行。
<%@ WebHandler Language="C#" Class="netpix.ImageGenerator" %>
这指示客户端请求由 ImageGenerator 类处理,该类将在下一节中讨论。
生成图片
ImageGenerator 类实现了 IHttpHandler 接口,这是一个非常简单且低级的接口,用于将原始数据流发送回客户端。我们只是转储图像的字节,所以它非常适合我们的需求。由于此类是应用程序运行的关键,我们将检查此类的所有代码。
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";
在这里,我们从 URL 中检索图片 ID,并调用 GetPathToPicture 方法,该方法封装了一个 SQL join 语句,返回图像的完整本地路径以及图像在客户端上被查看的次数。然后,我们将内容类型设置为 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 = 150.0f / 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);
        thumb.Save(Context.Response.OutputStream, 
            System.Drawing.Imaging.ImageFormat.Jpeg);
        bmp.Dispose();
        thumb.Dispose();
    }
如果请求 URL 包含 thumbnail 参数,我们首先从磁盘加载图像文件,并调用 GetThumbnailImage 来缩小它。我们按恒定比例缩小它以保持纵横比,以免扭曲图像。然后,我们将调整大小后的图像直接保存到 Response 对象的输出流中。当请求大量缩略图时,这会给服务器 CPU 带来很大的压力(我将在“未来项目”部分讨论这一点)。
    else
    {
        // 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;
        while( fs.Read(bytes, 0, byteLength ) != 0 )
        {
            Context.Response.BinaryWrite(bytes); 
        }
        fs.Close();
        // Now increment the view counter in the database
        npdata.SetNumViews(picid, numviews+1);
    }
} 
在这种情况下,我们有兴趣直接从文件内容流式传输图像。当前实现通过块读取文件内容,并将它们发送到响应的输出流。由于已请求全尺寸图像,我们还需要在数据库中增加图片的查看次数。SetNumViews 方法只是向 pics 表发出一个 SQL UPDATE 语句,以设置给定图片的 numviews 列。
查看和编辑图片

从相册视图中,用户可以查看图片或编辑图片信息。在 viewpic.aspx 或 editpic.aspx 中实际上并没有太多值得关注的内容。用户可以在编辑器中提供标题和描述信息,然后查看器将使用这些信息。如果提供了标题,查看器将显示图片的标题,否则将默认为文件名。这在 SQL 语句中实现。
    // The command to select a specific picture data
    getpicinfo = new SqlCommand("SELECT ISNULL(title, filename) 
                       AS returntitle, " +
                       "ISNULL([description],'') AS returndesc "+
                       "FROM pics WHERE pics.id=@picid", conn);
    getpicinfo.Parameters.Add("@picid", SqlDbType.Int);
我们的架构设计规定,我们将使用 DB Null 值来表示没有自定义标题。在这种情况下,我们使用图像文件名作为图片标题文本。
未来/待办事项
此应用程序仅代表一个健壮的图像编目和查看系统的基本框架。可以实现的一些想法:
- 将缩略图缓存到应用程序的 Cache 字典中。设置文件系统依赖项,以便在图像在磁盘上发生更改时自动使缓存项失效。
- 可以通过向数据库添加用户表,并在相册表中添加另一个外键列来标识用户所属的相册,来实现多用户支持。
- 可以在 pics表中存储更多信息:颜色数量等,并支持更多格式(例如 GIF)。
- 添加功能,通过 Web 表单指定电子邮件地址来邀请用户查看您的图片。
- 如前所述,动态生成缩略图会给服务器 CPU 带来很大的负载。在流量大的环境中,最好由后端生成器创建缩略图,并将其存储在数据库本身或文件系统中的已知位置。
- 许多其他想法...
历史
- 初始版本:2003 年 7 月 27 日
- 更新:2003 年 6 月 29 日 - 实现了一些根据反馈提出的建议。
