65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (47投票s)

2003年7月27日

11分钟阅读

viewsIcon

418710

downloadIcon

13266

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

Sample Image

引言

此应用程序提供了一些基本的照片共享功能,类似于 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 文件。未来的增强功能可能包括递归扫描所有子目录中的所有文件。

使用代码

在阅读本文之前,您可能希望安装并运行代码。为此,您应该按照以下步骤操作

  1. 下载 后端 Windows Forms 应用 并将其解压缩到硬盘上的某个位置。
  2. 编辑应用程序的配置文件,以指定您的 SQL 服务器和包含图片的根文件夹。该文件名为 App.Config,但请注意,VS.NET 在构建项目时会自动复制并重命名此文件为 NPdbman.exe.config
  3. 运行 NPdbman.exe 应用程序,并从数据库菜单中选择“Initialize”。这将创建一个名为 netpix 的新 SQL 数据库,并创建其中的表和约束。
  4. 从数据库菜单中选择“Build”。这将使用从配置文件中指定的文件夹扫描的信息填充表。
  5. 下载 前端 ASP.NET 应用 并将其解压缩到 IIS 的 wwwroot 文件夹中。运行 IIS 配置工具,右键单击 netpix 文件夹。选择“Properties”,然后在“Application Settings”窗格中,选择“Create”。
  6. 编辑应用程序的 web.config 文件,并指定您的 SQL 服务器连接。
  7. 现在您应该可以浏览 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.sqlcreatealbum.sqlcreatepics.sql 执行的。这些脚本文件包含了所有必需的 SQL 语句,用于删除然后 CREATE TABLEALTER 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 之后,它应该获取存储过程的输出参数,并使用此值(标识值)来更新断开连接的 DataTableid 列。

// 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.aspxeditpic.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 日 - 实现了一些根据反馈提出的建议。
© . All rights reserved.