上载文件 (C#)
作者 :斯科特·米切尔
了解如何允许用户将二进制文件(如 Word 或 PDF 文档)上传到网站,这些文件可能存储在服务器的文件系统或数据库中。
介绍
到目前为止,我们检查的所有教程都专门处理了文本数据。 但是,许多应用程序都有捕获文本和二进制数据的数据模型。 在线约会网站可能允许用户上传图片以与其个人资料关联。 招聘网站可能允许用户将其简历作为Microsoft Word 或 PDF 文档上传。
使用二进制数据会增加一组新的挑战。 我们必须确定二进制数据存储在应用程序中的方式。 必须更新用于插入新记录的界面,以允许用户从其计算机上传文件,必须执行额外的步骤才能显示或提供下载记录关联的二进制数据的方法。 在本教程和接下来的三个教程中,我们将探讨如何阻碍这些挑战。 在本教程结束时,我们将构建一个功能齐全的应用程序,用于将图片和 PDF 小册子与每个类别相关联。 在此特定教程中,我们将介绍用于存储二进制数据的不同技术,并探讨如何让用户从其计算机上传文件并将其保存在 Web 服务器的文件系统上。
注意
作为应用程序数据模型的一部分的二进制数据有时称为 BLOB,这是二进制大型 OBject 的首字母缩略词。 在这些教程中,我选择了使用术语二进制数据,尽管术语 BLOB 是同义词。
步骤 1:创建使用二进制数据网页
在开始探索与添加对二进制数据的支持相关的挑战之前,让我们先花点时间在网站项目中创建 ASP.NET 页面,本教程和接下来的三个页面。 首先添加名为 <Site.master
母版页相关联:
Default.aspx
FileUpload.aspx
DisplayOrDownloadData.aspx
UploadInDetailsView.aspx
UpdatingAndDeleting.aspx
图 1:为二进制数据相关教程添加 ASP.NET 页
与其他文件夹中一样, Default.aspx
该 BinaryData
文件夹中将列出其部分中的教程。 回想一下, SectionLevelTutorialListing.ascx
用户控件提供了此功能。 因此,通过将此用户控件从解决方案资源管理器拖动到Default.aspx
页面的设计视图中,将其添加到该控件。
图 2:将用户控件Default.aspx
添加到 SectionLevelTutorialListing.ascx
(单击以查看全尺寸图像)
最后,将这些页面添加为文件的条目 Web.sitemap
。 具体而言,在增强 GridView <siteMapNode>
后添加以下标记:
<siteMapNode
title="Working with Binary Data"
url="~/BinaryData/Default.aspx"
description="Extend the data model to include collecting binary data.">
<siteMapNode
title="Uploading Files"
url="~/BinaryData/FileUpload.aspx"
description="Examine the different ways to store binary data on the
web server and see how to accept uploaded files from users
with the FileUpload control." />
<siteMapNode
title="Display or Download Binary Data"
url="~/BinaryData/DisplayOrDownloadData.aspx"
description="Let users view or download the captured binary data." />
<siteMapNode
title="Adding New Binary Data"
url="~/BinaryData/UploadInDetailsView.aspx"
description="Learn how to augment the inserting interface to
include a FileUpload control." />
<siteMapNode
title="Updating and Deleting Existing Binary Data"
url="~/BinaryData/UpdatingAndDeleting.aspx"
description="Learn how to update and delete existing binary data." />
</siteMapNode>
更新 Web.sitemap
后,请花点时间通过浏览器查看教程网站。 左侧菜单现在包括“使用二进制数据”教程的项。
图 3:站点地图现在包含使用二进制数据教程的条目
步骤 2:确定存储二进制数据的位置
与应用程序数据模型关联的二进制数据可以存储在以下两个位置之一:在 Web 服务器文件系统上,引用存储在数据库中的文件;或直接在数据库本身内(见图 4)。 每个方法都有自己的一组利弊,值得更详细的讨论。
图 4:二进制数据可存储在文件系统上或直接存储在数据库中(单击可查看全尺寸图像)
假设我们想要扩展 Northwind 数据库,以便将图片与每个产品相关联。 一个选项是将这些映像文件存储在 Web 服务器的文件系统上 Products
,并记录表中的路径。 使用此方法,我们可能会向Products
类型的varchar(200)
表添加一列ImagePath
。 当用户上传 Chai 的图片时,该图片可能存储在 Web 服务器文件系统 ~/Images/Tea.jpg
中,其中 ~
表示应用程序的物理路径。 也就是说,如果网站植根于物理路径 C:\Websites\Northwind\
, ~/Images/Tea.jpg
则等效于 C:\Websites\Northwind\Images\Tea.jpg
。 上传图像文件后,我们将更新表中的 Chai 记录 Products
,以便其 ImagePath
列引用新图像的路径。 我们可以使用 ~/Images/Tea.jpg
或只是 Tea.jpg
如果我们决定所有产品图像将放置在应用程序 Images
文件夹中。
在文件系统上存储二进制数据的主要优点包括:
- 轻松实现 ,因为我们很快就会看到,存储和检索直接存储在数据库中的二进制数据比通过文件系统处理数据时多一些代码。 此外,为了使用户能够查看或下载二进制数据,必须向用户显示该数据的 URL。 如果数据驻留在 Web 服务器文件系统上,则 URL 非常简单。 但是,如果数据存储在数据库中,则需要创建一个网页,以便从数据库检索和返回数据。
- 其他服务或应用程序可能需要访问二进制数据,而无法从数据库拉取数据的二进制数据 。 例如,可能还需要通过 FTP 向用户提供与每个产品关联的图像,在这种情况下,我们希望将二进制数据存储在文件系统上。
- 如果二进制数据存储在文件系统上,则数据库服务器和 Web 服务器之间的需求和网络拥塞将小于直接存储在数据库中的二进制数据时的性能。
在文件系统上存储二进制数据的主要缺点是它将数据与数据库分离。 如果从 Products
表中删除记录,则不会自动删除 Web 服务器文件系统上的关联文件。 我们必须编写额外的代码来删除文件,否则文件系统将变得杂乱无章,未使用的孤立文件。 此外,备份数据库时,还必须确保备份文件系统上关联的二进制数据。 将数据库移动到另一个站点或服务器会带来类似的挑战。
或者,可以通过创建类型 varbinary
列直接存储在Microsoft SQL Server 2005 数据库中的二进制数据。 与其他可变长度数据类型一样,可以指定可在此列中保存的二进制数据的最大长度。 例如,若要保留最多 5,000 个字节,varbinary(5000)
varbinary(MAX)
则允许最大存储大小(约 2 GB)。
直接在数据库中存储二进制数据的主要优点是二进制数据和数据库记录之间的紧密耦合。 这大大简化了数据库管理任务,例如备份或将数据库移动到其他站点或服务器。 此外,删除记录会自动删除相应的二进制数据。 在数据库中存储二进制数据也有更微妙的好处。 有关更深入的讨论,请参阅 使用 ASP.NET 2.0 直接在数据库中存储二进制文件。
注意
在 Microsoft SQL Server 2000 和更早版本中, varbinary
数据类型的最大限制为 8,000 字节。 若要存储多达 2 GB 的二进制数据, image
需要改用数据类型 。 但是,随着 SQL Server 2005 中的添加 MAX
, image
数据类型已弃用。 它仍支持向后兼容性,但Microsoft已宣布将在 image
SQL Server 的未来版本中删除该数据类型。
如果使用较旧的数据模型,可能会看到 image
数据类型。 Northwind 数据库表 Categories
具有一个 Picture
列,可用于存储类别图像文件的二进制数据。 由于 Northwind 数据库根植于 Microsoft Access 和早期版本的 SQL Server 中,因此此列的类型为类型 image
。
对于本教程和接下来的三种方法,我们将使用这两种方法。 该 Categories
表已有一列 Picture
用于存储类别图像的二进制内容。 我们将添加一个附加列 BrochurePath
,用于在 Web 服务器上的文件系统上存储 PDF 的路径,该文件系统可用于提供类别的打印质量、完善的概述。
步骤 3:将BrochurePath
列添加到Categories
表
目前,“类别”表只有四列: CategoryID
、 CategoryName
、 Description
和 Picture
。 除了这些字段,我们需要添加一个新的字段,它将指向类别的小册子(如果存在)。 若要添加此列,请转到服务器资源管理器,向下钻取到表,右键单击 Categories
表并选择“打开表定义”(请参阅图 5)。 如果未看到服务器资源管理器,请从“视图”菜单中选择“服务器资源管理器”选项,或按 Ctrl+Alt+S 将其打开。
varchar(200)
向已命名BrochurePath
的Categories
表添加新列,并允许 NULL
s 并单击“保存”图标(或按 Ctrl+S)。
图 5:向表添加 BrochurePath
列 Categories
(单击以查看全尺寸图像)
步骤 4:更新体系结构以使用Picture
列BrochurePath
数据CategoriesDataTable
访问层(DAL)当前定义了CategoryID
四DataColumn
个:、CategoryName
、Description
和NumberOfProducts
。 当我们最初在“创建数据访问层”教程中设计此 DataTable 时,CategoriesDataTable
只有前三列;该NumberOfProducts
列是在 Master/Detail 中使用带有 Details DataList 教程的“主记录项目符号列表”添加的。
如在创建数据访问层中所述,类型化 DataSet 中的 DataTable 组成业务对象。 TableAdapters 负责与数据库通信,并使用查询结果填充业务对象。 填充 CategoriesDataTable
方法 CategoriesTableAdapter
有三种数据检索方法:
GetCategories()
执行 TableAdapter 的主查询,并返回CategoryID
表中所有记录Categories
的、CategoryName
字段和Description
字段。 主查询是自动生成Insert
和Update
方法使用的内容。GetCategoryByCategoryID(categoryID)
返回CategoryID
其CategoryID
等于 categoryID 的类别的和CategoryName
Description
字段。GetCategoriesAndNumberOfProducts()
- 返回表中所有记录Categories
的CategoryID
和Description
CategoryName
字段。 此外,使用子查询返回与每个类别关联的产品数。
请注意,这些查询都不返回 Categories
表 Picture
或 BrochurePath
列;这些字段也不 CategoriesDataTable
返回 DataColumn
这些字段。 为了使用图片和 BrochurePath
属性,我们需要先将它们添加到 CategoriesDataTable
该类,然后更新 CategoriesTableAdapter
类以返回这些列。
Picture
添加 andBrochurePath``DataColumn
s
首先,将这两列添加到 。CategoriesDataTable
右键单击 CategoriesDataTable
s 标头,从上下文菜单中选择“添加”,然后选择“列”选项。 这将在名为 Column1
的 DataTable 中新建一个 DataColumn
。 将此列重命名为 Picture
. 从属性窗口中,将 DataColumn
s DataType
属性System.Byte[]
设置为(这不是下拉列表中的选项;需要键入它)。
图 6:创建DataColumn
Picture
名称(DataType
System.Byte[]
单击以查看全尺寸图像)
将另一个DataColumn
添加到 DataTable,并使用DataType
默认值命名它BrochurePath
(System.String
)。
从Picture
TableAdapter 返回和BrochurePath
值
在这两DataColumn
个版本中添加后CategoriesDataTable
,我们准备更新。CategoriesTableAdapter
我们可以在主 TableAdapter 查询中返回这两个列值,但每次调用该方法时 GetCategories()
都会返回二进制数据。 相反,让我们更新主 TableAdapter 查询,以返回 BrochurePath
并创建返回特定类别列 Picture
的其他数据检索方法。
若要更新主 TableAdapter 查询,请 CategoriesTableAdapter
右键单击 s 标头,然后从上下文菜单中选择“配置”选项。 此时会显示表适配器配置向导,我们在过去的许多教程中已看到该向导。 更新查询以恢复 BrochurePath
并单击“完成”。
图 7:将语句中的 SELECT
列列表更新为“也返回 BrochurePath
”(单击以查看全尺寸图像)
对 TableAdapter 使用即席 SQL 语句时,更新主查询中的列列表会更新 TableAdapter 中所有 SELECT
查询方法的列列表。 这意味着方法 GetCategoryByCategoryID(categoryID)
已更新以返回 BrochurePath
列,这可能是我们预期的。 但是,它还更新了方法中的 GetCategoriesAndNumberOfProducts()
列列表,删除返回每个类别的产品数的子查询! 因此,我们需要更新此方法的 SELECT
查询。 右键单击 GetCategoriesAndNumberOfProducts()
该方法,选择“配置”,并将查询还原 SELECT
回其原始值:
SELECT CategoryID, CategoryName, Description,
(SELECT COUNT(*)
FROM Products p
WHERE p.CategoryID = c.CategoryID)
as NumberOfProducts
FROM Categories c
接下来,创建一个新的 TableAdapter 方法,该方法返回特定类别的 Picture
列值。 右键单击 CategoriesTableAdapter
s 标头,然后选择“添加查询”选项以启动 TableAdapter 查询配置向导。 此向导的第一步询问我们是否要使用即席 SQL 语句、新的存储过程或现有存储过程查询数据。 选择“使用 SQL 语句”,然后单击“下一步”。 由于我们将返回行,因此请选择从第二步返回行选项的 SELECT。
图 8:选择“使用 SQL 语句”选项(单击以查看全尺寸图像)
图 9:由于查询将返回类别表中的记录,请选择返回行的 SELECT (单击以查看全尺寸图像)
在第三步中,输入以下 SQL 查询并单击“下一步” :
SELECT CategoryID, CategoryName, Description, BrochurePath, Picture
FROM Categories
WHERE CategoryID = @CategoryID
最后一步是选择新方法的名称。 GetCategoryWithBinaryDataByCategoryID
分别用于FillCategoryWithBinaryDataByCategoryID
填充 DataTable 和返回 DataTable 模式。 单击“完成”,完成向导。
图 10:选择 TableAdapter 方法的名称(单击以查看全尺寸图像)
注意
完成表适配器查询配置向导后,你可能会看到一个对话框,告知新命令文本返回与主查询架构不同的架构的数据。 简言之,向导指出 TableAdapter 的主查询 GetCategories()
返回的架构不同于我们刚刚创建的架构。 但这就是我们想要的,因此你可以忽略此消息。
此外,请记住,如果使用即席 SQL 语句并使用向导在稍后某个时间点更改 TableAdapter 主查询,它将修改 GetCategoryWithBinaryDataByCategoryID
方法的 SELECT
语句列列表,使其仅包含主查询中的这些列(即,它将从查询中删除 Picture
该列)。 必须手动更新列列表才能返回 Picture
列,这类似于我们在此步骤前面的方法中执行的操作 GetCategoriesAndNumberOfProducts()
。
将两DataColumn
个 s 添加到CategoriesDataTable
GetCategoryWithBinaryDataByCategoryID
和方法添加到CategoriesTableAdapter
后,类型化数据集设计器中的这些类应类似于图 11 中的屏幕截图。
图 11:数据集设计器包含新列和方法
更新业务逻辑层 (BLL)
更新 DAL 后,剩余的所有操作都是扩充业务逻辑层(BLL),以包含新 CategoriesTableAdapter
方法的方法。 将下列方法添加到 CategoriesBLL
类:
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.CategoriesDataTable
GetCategoryWithBinaryDataByCategoryID(int categoryID)
{
return Adapter.GetCategoryWithBinaryDataByCategoryID(categoryID);
}
步骤 5:将文件从客户端上传到 Web 服务器
收集二进制数据时,最终用户通常会提供此数据。 若要捕获此信息,用户需要能够将文件从计算机上传到 Web 服务器。 然后,上传的数据需要与数据模型集成,这可能意味着将文件保存到 Web 服务器文件系统,并将路径添加到数据库中的文件,或将二进制内容直接写入数据库中。 在此步骤中,我们将了解如何允许用户将文件从计算机上传到服务器。 在下一个教程中,我们将把注意力转向将上传的文件与数据模型集成。
ASP.NET 2.0 s 新的 FileUpload Web 控件 为用户提供将文件从计算机发送到 Web 服务器的机制。 FileUpload 控件呈现为一个 <input>
元素,其 type
属性设置为文件,浏览器将显示为带有“浏览”按钮的文本框。 单击“浏览”按钮会显示一个对话框,用户可以从中选择文件。 当表单发回时,所选文件的内容随回发一起发送。 在服务器端,可通过 FileUpload 控件的属性访问有关上传的文件的信息。
若要演示如何上传文件,请打开 FileUpload.aspx
文件夹中的页面,将工具箱中的 BinaryData
FileUpload 控件拖到设计器上,并将控件 ID
的属性设置为 UploadTest
。 接下来,分别添加按钮 Web 控件设置其属性ID
以及Text
UploadButton
上传所选文件的属性。 最后,将标签 Web 控件放在 Button 下面,清除其 Text
属性并将其属性设置为 ID
UploadDetails
。
图 12:将 FileUpload 控件添加到 ASP.NET 页(单击以查看全尺寸图像)
图 13 显示通过浏览器查看此页面。 请注意,单击“浏览”按钮将显示文件选择对话框,允许用户从计算机中选择文件。 选择文件后,单击“上传所选文件”按钮会导致回发将所选文件的二进制内容发送到 Web 服务器。
图 13:用户可以选择要从计算机上传到服务器的文件(单击以查看全尺寸图像)
在回发时,上传的文件可以保存到文件系统,或者可以通过 Stream 直接处理其二进制数据。 在本示例中,让我们创建一个 ~/Brochures
文件夹,并将上传的文件保存到该文件夹中。 首先,将 Brochures
文件夹作为根目录的子文件夹添加到站点。 接下来,为 UploadButton
s Click
事件创建事件处理程序并添加以下代码:
protected void UploadButton_Click(object sender, EventArgs e)
{
if (UploadTest.HasFile == false)
{
// No file uploaded!
UploadDetails.Text = "Please first select a file to upload...";
}
else
{
// Display the uploaded file's details
UploadDetails.Text = string.Format(
@"Uploaded file: {0}<br />
File size (in bytes): {1:N0}<br />
Content-type: {2}",
UploadTest.FileName,
UploadTest.FileBytes.Length,
UploadTest.PostedFile.ContentType);
// Save the file
string filePath =
Server.MapPath("~/Brochures/" + UploadTest.FileName);
UploadTest.SaveAs(filePath);
}
}
FileUpload 控件提供了各种用于处理上传数据的属性。 例如,该 HasFile
属性 指示用户是否上传了文件,而 FileBytes
该属性 以字节数组的形式提供对上传的二进制数据的访问权限。 Click
事件处理程序首先确保文件已上传。 如果文件已上传,则标签会显示上传文件的名称、其大小(以字节为单位)及其内容类型。
注意
若要确保用户上传文件,可以检查 HasFile
属性并显示警告(如果为 false
该属性,也可以改用 RequiredFieldValidator 控件 )。
FileUpload s SaveAs(filePath)
将上传的文件保存到指定的 filePath。 filePath 必须是物理路径(C:\Websites\Brochures\SomeFile.pdf
)而不是虚拟路径(/Brochures/SomeFile.pdf
)。 该方法Server.MapPath(virtPath)
采用虚拟路径并返回其相应的物理路径。 此处,虚拟路径是 ~/Brochures/fileName
fileName 是上传文件的名称。 有关虚拟路径和物理路径和使用Server.MapPath
的详细信息,请参阅 Server.MapPath 方法。
完成 Click
事件处理程序后,花点时间在浏览器中测试页面。 单击“浏览”按钮,然后从硬盘驱动器中选择一个文件,然后单击“上传所选文件”按钮。 回发会将所选文件的内容发送到 Web 服务器,然后在将文件保存到 ~/Brochures
文件夹之前显示有关该文件的信息。 上传文件后,返回到 Visual Studio,然后单击解决方案资源管理器中的“刷新”按钮。 应会看到刚刚在 ~/Brochures 文件夹中上传的文件!
图 14:文件 EvolutionValley.jpg
已上传到 Web 服务器(单击以查看全尺寸图像)
图 15: EvolutionValley.jpg
已保存到 ~/Brochures
文件夹
将上传的文件保存到文件系统的细微之处
将文件上传到 Web 服务器文件系统时,必须解决几个细微问题。 首先,存在安全问题。 若要将文件保存到文件系统,执行 ASP.NET 页的安全上下文必须具有写入权限。 ASP.NET 开发 Web 服务器在当前用户帐户的上下文下运行。 如果使用 Microsoft Internet Information Services(IIS)作为 Web 服务器,安全上下文取决于 IIS 的版本及其配置。
将文件保存到文件系统的另一个挑战是命名文件。 目前,我们的页面使用与客户端计算机上的文件相同的名称将所有上传的文件 ~/Brochures
保存到目录中。 如果用户 A 上传名称为 Brochure.pdf
小册子,该文件将另存为 ~/Brochure/Brochure.pdf
。 但是,如果稍后用户 B 上传另一个恰好具有相同文件名(Brochure.pdf
)的小册子文件,该怎么办? 使用我们现在拥有的代码,用户 A 文件将被用户 B 上传覆盖。
有多种方法可用于解决文件名冲突。 一个选项是禁止上传文件(如果已存在同名文件)。 使用此方法时,当用户 B 尝试上传名为 Brochure.pdf
的文件时,系统不会保存其文件,而是显示一条消息,告知用户 B 重命名该文件,然后重试。 另一种方法是使用唯一文件名保存文件,这可能是全局唯一 标识符(GUID) 或相应数据库记录主键列(假设上传与数据模型中的特定行关联)的值。 在下一教程中,我们将更详细地探讨这些选项。
涉及大量二进制数据的挑战
这些教程假定捕获的二进制数据大小适中。 使用大量的二进制数据文件(几兆字节或更大的文件)会带来超出这些教程范围的新挑战。 例如,默认情况下,ASP.NET 将拒绝超过 4 MB 的上传,尽管可以通过元素来Web.config
配置<httpRuntime>
。 IIS 也施加了自己的文件上传大小限制。 此外,上传大型文件所需的时间可能超过默认 110 秒,ASP.NET 将等待请求。 使用大型文件时,还会出现内存和性能问题。
对于大型文件上传,FileUpload 控件是不切实际的。 由于文件的内容正在发布到服务器,最终用户必须耐心地等待,而不会确认其上传正在进行。 处理可以在几秒钟内上传的较小文件时,这不是一个问题,但在处理可能需要几分钟才能上传的较大文件时可能会出现问题。 有许多第三方文件上传控件更适合处理大型上传,其中许多供应商提供进度指示器和 ActiveX 上传管理器,这些控件提供更完善的用户体验。
如果应用程序需要处理大型文件,则需要仔细调查挑战,并找到适合特定需求的解决方案。
总结
生成需要捕获二进制数据的应用程序会带来许多挑战。 在本教程中,我们探讨了前两个教程:决定在何处存储二进制数据并允许用户通过网页上传二进制内容。 在接下来的三个教程中,我们将了解如何将上传的数据与数据库中的记录相关联,以及如何与其文本数据字段一起显示二进制数据。
快乐编程!
深入阅读
有关本教程中讨论的主题的详细信息,请参阅以下资源:
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0。 他可以通过他的博客联系到mitchell@4GuysFromRolla.com他,可以在该博客中找到http://ScottOnWriting.NET。
特别感谢
本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy 和 Bernadette Leigh。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请把我扔一条线。mitchell@4GuysFromRolla.com