一个 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 日 - 实现了一些根据反馈提出的建议。