上载文件53

简介

目前为止 , 我们介绍的所有教程都是专门处理文本数据。但是,许多应用程序的数据模型既可以使用文本数据,也可以使用二进制数据。在线约会网站可能允许用户上载与其个人简介有关的图片。招聘网站可能允许用户以 Microsoft Word 或 PDF 文档格式上载他们的简历。

处理二进制数据增加了一系列新的挑战。我们必须确定在应用程序中如何存储二进制数据。必须对插入新记录的界面进行更新,使用户可以从他们的计算机上载文件,并且必须采取额外的步骤来显示与记录相关的二进制数据或提供一种方法来下载与记录相关的二进制数据。在本教程和接下来的三个教程中,我们将介绍如何克服这些挑战。在这些教程的最后,我们将构建一个具有全功能的应用程序,将图片或 PDF 小册子与每种类别相关联。在此特定教程中,我们将了解用于存储二进制数据的各种不同技术,还将介绍如何使用户从他们的计算机上载文件,并将文件保存在 Web 服务器的文件系统上。

注意 : 二进制数据是应用程序的数据模型的一部分 , 有时称为 BLOB , 二进制大型对象的首字母缩写。在这些教程中,我选择使用术语“二进制数据”,尽管术语 BLOB 是同义词。

步骤1 :创建使用二进制数据的网页

在我们探讨与添加对二进制数据的支持相关的挑战之前 , 让我们首先花些时间在网站项目中创建几个ASP.NET 页 , 在本教程和后面的三篇教程中我们将用到它们。首先添加一个名称为 BinaryData 的文件夹。接下来,将以下 ASP.NET 页添加到该文件夹,确保每个页与 Site.master 母版页相关联:

  • Default.aspx
  • FileUpload.aspx
  • DisplayOrDownloadData.aspx
  • UploadInDetailsView.aspx
  • UpdatingAndDeleting.aspx

图1 : 为与二进制数据相关的教程添加 ASP.NET 页

像在其他文件夹中一样 ,BinaryData 文件夹中的 Default.aspx 列出教程的章节。记得 SectionLevelTutorialListing.ascx 用户控件可以提供此功能。因此,通过将控件从Solution Explorer 拖放到页面的 Design 视图来添加此用户控件。

图2 : 将SectionLevelTutorialListing.ascx 用户控件添加至 Default.aspx

最后 , 将页面按条目添加到 Web.sitemap 文件中。具体来说 , 在 “Enhancing the 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 之后 , 花些时间使用浏览器查看一下教程网站。左侧的菜单现在包含 Working with Binary Data 教程的条目。

图3 : 网站地图现在包含 Working with Binary Data 教程的条目。

步骤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 。上载图像文件之后,我们将更新 Products 表中的 Chai 记录,以便表中的 ImagePath 列引用新图像的路径。如果我们确定所有产品的图像都将放置在应用程序的Images 文件夹中 , 则可以使用 “~/Images/Tea.jpg” 或只使用 “Tea.jpg” 。

将二进制数据存储在文件系统上的主要优点包括 :

  • 易于实现– 正如我们马上要看到的,存储和检索直接存储在数据库中的二进制数据比通过文件系统处理数据包含更多的代码。此外,为了使用户查看或下载二进制数据,必须向用户提供该数据的 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 已经宣布在 SQL Server 的未来版本中将删除 image 数据类型。

如果您正在使用旧的数据模型,可能会看到 image 数据类型。Northwind 数据库的 Categories 表有一个 Picture 列 , 可以用于存储类别图像文件的二进制数据。由于Northwind 数据库源于 Microsoft Access 和SQL Server 的早期版本 , 此列的类型是 image 。

对于本教程和后面的三个教程 , 我们将使用两种存储方法。 Categories 表已经有了一个用于存储类别图像的二进制内容的 Picture 列。我们将添加另外一个 BrochurePath 列,用于存储到 Web 服务器的文件系统上的 PDF 文件的路径,此 PDF 文件可以用来提供类别的打印质量的优美视图。

步骤3:向 Categories 表添加 BrochurePath 列

当前的Categories 表只有四列 :CategoryID 、CategoryName 、Description 和 Picture 。除了这些字段之外,我们需要添加一个指向类别的小册子(如果存在)的新的列。为添加此列 , 转到 Server Explorer , 深入到表中 , 右键单击Categories 表 , 然后选择 “Open Table Definition” ( 参见图 5 ) 。如果看不到 Server Explorer , 通过从 View 菜单中选择 Server Explorer 选项或按 Ctrl+Alt+S 。

向Categories 表添加一个新的类型为 varchar(200) 、 名称为BrochurePath 的列并允许其值为 NULL , 然后单击 Save 图标 ( 或按 Ctrl+S ) 。

图5 : 向 Categories 表添加一个 BrochurePath 列

步骤4 : 将架构更新为使用 Picture 和BrochurePath 列

数据访问层(DAL) 中的 CategoriesDataTable 目前有四个定义的 DataColumns :CategoryID 、CategoryName 、Description 和 NumberOfProducts 。我们原先在 创建数据访问层 教程中设计此 DataTable 时 ,CategoriesDataTable 只有前三列 ;NumberOfProducts 列是在 使用具有 Details DataList 的主要记录项目符号列表的主/明细报表Master 报表的 Master/Detail 教程中添加的。

正如在创建数据访问层 中所介绍的 ,Typed DataSet ( 强类型 DataSet ) 中的 DataTables 组成业务对象。 TableAdapters 负责与数据库进行通信并使用查询结果填充业务对象。 CategoriesDataTable 由 CategoriesTableAdapter 来赋值, CategoriesTableAdapter 有三种数据检索方法:

  • GetCategories() – 执行 TableAdapter 的主查询并返回 Categories 表中所有记录的 CategoryID 、CategoryName 和 Description 字段。自动生成的 Insert 和 Update 方法使用的就是主查询。
  • GetCategoryByCategoryID(categoryID)– 返回 CategoryID 等于 categoryID 的类别的 CategoryID 、CategoryName 和 Description 字段。
  • GetCategoriesAndNumberOfProducts()– 返回 Categories 表中所有记录的 CategoryID 、CategoryName 和 Description 字段。还使用子查询返回与每种类别相关联的产品的数目。

请注意 , 这些查询没有一个返回 Categories table 的 Picture 或 BrochurePath 列 ;CategoriesDataTable 也没有为这些字段提供 DataColumns 。为了使用 Picture 和 BrochurePath 属性 , 我们需要首先将它们添加到 CategoriesDataTable , 然后将CategoriesTableAdapter 类更新为返回这些列。

添加名为Picture 和BrochurePath 的DataColumn

首先向CategoriesDataTable 添加这两列。右键单击CategoriesDataTable 的标题 , 从上下文菜单中选择 Add , 然后选择Column 选项。这将在 DataTable 中创建一个名称为Column1 的 DataColumn 。将此列重命名为 Picture 。在 Properties 窗口中 , 将DataColumn 的 DataType 属性设置为“System.Byte[]” ( 这不是下拉列表中的选项 ; 需要输入 ) 。

图6 :创建一个数据类型为System.Byte[] 、名称为Picture 的 DataColumn

向DataTable 添加另一个 DataColumn , 使用默认的 DataType 值 (System.String) 将其命名为 BrochurePath 。

从TableAdapter 返回Picture 和BrochurePath 的值

将这两个DataColumns 添加到 CategoriesDataTable 之后 , 我们可以更新CategoriesTableAdapter 了 。我们可以在主 TableAdapter 查询中返回这两列的值 , 但是这将使每次调用 GetCategories() 方法都返回二进制数据。相反 , 让我们将主 TableAdapter 查询更新为返回 BrochurePath , 并创建另一个返回特定类别的 Picture 列的数据检索方法。

若要更新主TableAdapter 查询 , 在 CategoriesTableAdapter 的标题上单击右键 , 然后从上下文菜单中选择Configure 选项。这将打开 Table Adapter Configuration 向导 , 在以前的许多教程中我们已经看到过它。更新查询以返回 BrochurePath ,然后单击 Finish 。

图7 :在 SELECT 语句中更新 Column 列表也可以返回 BrochurePath

当对 TableAdapter 使用ad-hoc SQL 语句时 , 在主查询中更新列列表可以为TableAdapter 中的所有 SELECT 查询方法更新列列表。这意味着GetCategoryByCategoryID(categoryID) 方法已被更新为返回BrochurePath 列 , 这可能是我们所期望的。但是 , 它也在GetCategoriesAndNumberOfProducts() 方法中更新了列列表 , 删除了返回每种类别的产品数目的子查询 ! 因此 , 我们还需要更新 GetCategoriesAndNumberOfProducts() 方法的SELECT 查询。右键单击 GetCategoriesAndNumberOfProducts() 方法 , 选择 Configure , 使SELECT 查询恢复到它的原始值 :

SELECT CategoryID, CategoryName, Description,  
       (SELECT COUNT(*)  
            FROM Products p  
            WHERE p.CategoryID = c.CategoryID)  
       as NumberOfProducts 
FROM Categories c

接下来 , 创建一个返回特定类别的 Picture 列的值的新 TableAdapter 方法。右键单击 CategoriesTableAdapter 的标题 , 然后选择 Add Query 选项以启动 TableAdapter Query Configuration 向导。向导的第一步询问我们是否想要使用 ad-hoc SQL 语句、新的存储过程或现有的存储过程来查询数据。选择“Use SQL statements” , 然后单击 Next 。由于我们将返回一行 , 因而在第二步中选择 “SELECT which returns rows” 选项。

图8 :选择 “Use SQL statements” 选项

图9 :由于查询将从 Categories 表返回一条记录 , 因此选择 “SELECT which returns rows”

在第三步中 , 输入以下 SQL 语句 , 然后单击 Next :

SELECT     CategoryID, CategoryName, Description, BrochurePath, Picture 
FROM       Categories 
WHERE      CategoryID = @CategoryID

最后一步是为新方法选择名称。对 Fill a DataTable 模式和 Return a DataTable 模式分别使用 FillCategoryWithBinaryDataByCategoryID 和GetCategoryWithBinaryDataByCategoryID 。单击Finish 完成向导。

图10 :为 TableAdapter 的方法选择名称

注意 : 完成 Table Adapter Query Configuration 向导之后 , 可以看到一个对话框 , 通知您 “the new command text returns data with schema different from the schema of the main query” 。总之 , 向导提示TableAdapter 的主查询 – GetCategories() – 返回与我们刚创建的不同 schema 。但是,这是我们想要的,因此可以忽略此消息。

此外 , 记住 : 如果在以后的某些时候及时使用 ad-hoc SQL 语句和使用向导来更改 TableAdapter 的主查询 , 将会修改 GetCategoryWithBinaryDataByCategoryID 方法的 SELECT 语句的列列表 , 以包含来自主查询的那些列 ( 即 , 将从查询中删除 Picture 列 ) 。必须手工更新列列表以便返回 Picture 列 , 与前面我们在此步骤中对 GetCategoriesAndNumberOfProducts() 方法所做的一样。

向CategoriesDataTable 添加两个 DataColumns 和向 CategoriesTableAdapter 添加 GetCategoryWithBinaryDataByCategoryID 方法之后 ,Typed DataSet 设计器中的这些类应该与图 11 中的屏幕快照类似。

图11 :DataSet 设计器包含新的列和方法

更新业务逻辑层(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 服务器的文件系统并在数据库中添加文件的路径,或直接将二进制内容写入数据库。在本步骤中,我们将了解如何使用户从自己的计算机向 Web 服务器上载文件。在下一教程中,我们将把注意力转向使上载的文件和数据模型相结合。

ASP.NET 2.0 的新 FileUpload Web 控件 为用户提供了一种从他们自己的计算机向Web 服务器发送文件的机制。 FileUpload 控件呈现为一个 <input> 元素,它的 type 属性设置为 “file” ,浏览器将它显示为一个带有 Browse 按钮的文本框。 . 单击 Browse 按钮将调出一个对话框,用户可以从该对话框选择文件。当回传表单时,发送所选文件的内容和回传。在服务器端,通过 FileUpload 控件的属性可以访问有关上载文件的信息。

要演示上载文件 , 打开BinaryData 文件夹中的 FileUpload.aspx 页 , 从工具箱将一个 FileUpload 控件拖放到设计器上 , 并将控件的 ID 属性设置为 UploadTest 。接下来 , 添加一个 Web 按钮控件 , 分别将它的 ID 和 Text 属性设置为UploadButton 和 “Upload Selected File” 。最后 , 在 Button 下方放置一个 Web 标签控件 , 清除它的 Text 属性并将它的 ID 属性设置为 UploadDetails 。

图12 : 向 ASP.NET 页添加一个 FileUpload 控件

图13 显示通过浏览器查看时的此页面。请注意 , 单击 Browse 按钮可以调出一个文件选择对话框 , 允许用户从他们的计算机上选择文件。一旦选择了一个文件 , 单击“Upload Selected File” 按钮可以引起回传 , 将所选文件的二进制内容发送到Web 服务器。

图13 :用户可以从他们的计算机上选择文件上载到Web 服务器

回传时 , 上载的文件可以保存到文件系统 , 或者直接通过流来处理文件的二进制数据。对于此示例,我们创建一个 ~/Brochures 文件夹,将上载的文件保存在此处。首先将 Brochures 文件夹做为根目录的子文件夹添加到站点。然后,为UploadButton 的 Click 事件创建 Event Handler 并添加以下代码 :

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 Event Handler 首先确保已经上载了文件。如果已经上载了文件, Label 将显示上载的文件的名称、以字节为单位的文件大小和文件内容的类型。

注意 : 为确保用户上载了文件 , 可以检查HasFile 属性 , 如果它为 False , 则显示警告信息 , 或者改用RequiredFieldValidator 控件 。

FileUpload 的 SaveAs(filePath) 将上载的文件保存在指定的 filePath 。filePath 必须是物理路径(C:\Websites\Brochures\SomeFile.pdf) 而不是虚拟路径 (/Brochures/SomeFile.pdf) 。 Server.MapPath(virtPath) 方法 使用虚拟路径做为参数并返回它的相应的物理路径。此处 , 虚拟路径是~/Brochures/fileName ,其中fileName 是上载文件的名称。有关虚拟路径和物理路径以及使用Server.MapPath 的详细信息 , 请参阅 使用Server.MapPath 。

完成Click Event Handler 之后 , 花些时间在浏览器中测试页面。单击 Browse 按钮并从您的硬盘上选择一个文件 , 然后单击“Upload Selected File” 按钮。回传将把所选文件的内容发送到 Web 服务器, Web 服务器在将文件保存到 ~/Brochures 文件夹之前会显示有关该文件的信息。上载文件之后 , 返回到Visual Studio , 然后在 Solution Explorer 中单击Refresh 。应该在 ~/Brochures 文件夹中看到刚才上载的文件!

图14 :文件 EvolutionValley.jpg 已被上载到 Web 服务器

图15 :EvolutionValley.jpg 保存在 ~/Brochures 文件夹

将上载的文件保存到文件系统的一些细微之处

将上载的文件保存到 Web 服务器的文件系统时 , 有一些细微之处必须处理。首先是安全问题。要将文件保存到文件系统,执行 ASP.NET 所基于的安全上下文必须具有 Write 权限。 ASP.NET 开发 Web 服务器运行在您当前的用户帐户上下文中。如果您正在使用Microsoft’s Internet 信息服务 (IIS) 做为 Web 服务器 ,则 安全上下文取决于 IIS 的版本和它的配置。

将文件保存到文件系统的另一项挑战主要是文件的命名。当前,我们的页面将所有上载的文件都保存在 ~/Brochures 目录中,使用与客户端计算机上文件相同的名称。。如果用户A 上载一个名称为 Brochure.pdf 的小册子 , 文件将被保存为~/Brochure/Brochure.pdf 。但是,如果一段时间以后,用户 B 上载一个碰巧有相同文件名 (Brochure.pdf) 的不同的小册子,将会怎样?使用现有的代码,用户 A 的文件将会被用户 B 的上载覆盖。

有多种方法可以用于解决文件名冲突。一种选择是 : 在已经存在一个具有相同名称的文件时禁止上载该文件。使用这种方法 , 当用户B 试图上载名称为 Brochure.pdf 的文件时 , 系统将不保存这个文件 , 而是显示一条消息通知用户 B 重命名文件后再重试。另一种方法是使用唯一的文件名来保存文件,唯一文件名可以是 全局唯一标识符 (GUID) ,或者是相应数据库记录的主键列的值(假设上载的文件与数据模型中的特定行相关联)。在下一篇教程中,我们将更加详细地介绍这些选择。

特大规模的二进制数据面临的挑战

这些教程假定捕获的二进制数据的大小是适中的。处理特大规模的二进制数据– 几兆字节的文件或更大的文件 – 带来了新的挑战,这些内容已经超出了这些教程的范围。例如,默认情况下, ASP.NET 将拒绝上载超过 4MB 的数据,尽管可以通过 Web.config 中的 <httpRuntime> 元素 进行配置。 IIS 也有文件上载的大小限制。有关详细信息,请参阅 IIS 上载文件大小 。此外,上载大文件花费的时间可能超过 ASP.NET 等待一个请求所默认的 110 秒。处理大文件时还会引起内存和性能问题。

FileUpload 控件不适用于大文件上载。由于正在将文件的内容发送给服务器,最终用户必须耐心地等待,而没有任何确认上载进程的消息。当处理可以在几秒钟上载的小文件时,这不是什么问题,但是当处理可能花费数分钟上载的大文件,这就成为了问题。有各种更适合于处理大文件上载的第三方文件上载控件,许多供应商提供进度指示器和 ActiveX 上载管理器,可以提供更完美的用户体验。

如果您的应用程序需要处理大文件,您需要仔细研究这些挑战并找到适合您特定需求的适当解决方案。

小结

构建需要捕获二进制数据的应用程序带来了大量挑战。本教程中,我们探讨了前两个问题:确定在何处存储二进制数据和允许用户通过网页上载二进制内容。在后面的三个教程中,我们将了解怎样将上载的数据与数据库中的记录相关联,以及怎样在它的文本数据字段旁显示二进制数据。

快乐编程!

posted @ 2016-05-01 23:50  迅捷之风  阅读(101)  评论(0编辑  收藏  举报