作者 :斯科特·米切尔
在本教程中,我们将查看在网页上显示二进制数据的选项,包括图像文件的显示以及为 PDF 文件预配“下载”链接。
简介
在前面的教程中,我们探索了将二进制数据与应用程序基础数据模型关联的两种方法,并使用 FileUpload 控件将文件从浏览器上传到 Web 服务器文件系统。 我们尚未了解如何将上传的二进制数据与数据模型相关联。 也就是说,将文件上载并保存到文件系统后,文件的路径必须存储在相应的数据库记录中。 如果数据直接存储在数据库中,则上传的二进制数据不需要保存到文件系统,但必须注入到数据库中。
不过,在查看将数据与数据模型关联之前,让我们先看看如何向最终用户提供二进制数据。 呈现文本数据足够简单,但应如何呈现二进制数据? 当然,这取决于二进制数据类型。 对于图像,我们可能需要显示图像;对于 PDF、Microsoft Word 文档、ZIP 文件和其他类型的二进制数据,提供下载链接可能更合适。
在本教程中,我们将了解如何使用 GridView 和 DetailsView 等数据 Web 控件来呈现二进制数据及其关联的文本数据。 在下一个教程中,我们将注意将上传的文件与数据库相关联。
步骤 1:提供BrochurePath数值
Picture表中的Categories列已包含各种类别图像的二进制数据。 具体而言,每个记录的 Picture 列包含低质量、粗糙、16 色位图图像的二进制内容。 每个类别图像宽 172 像素,高 120 像素,消耗大约 11 KB。 更重要的是,列中的二进制内容 Picture 包括一个 78 字节 的 OLE 标头,该标头必须在显示图像之前去除。 此标头信息存在,因为 Northwind 数据库在其根位于 Microsoft Access 中。 在 Access 中,二进制数据是使用 OLE 对象数据类型存储的,该数据类型会附加一个标头。 目前,我们将了解如何从这些低质量图像中去除标题,以显示图片。 在今后的教程中,我们将创建一个接口,以更新某个类别的 Picture 列,并将使用 OLE 标头的位图图像替换为不含 OLE 标头的等效 JPG 图像。
在前面的教程中,我们了解了如何使用 FileUpload 控件。 因此,可以继续将小册子文件添加到 Web 服务器文件系统。 执行此操作不会更新Categories表中的BrochurePath列。 在下一个教程中,我们将了解如何完成此操作,但现在我们需要手动为此列提供值。
在本教程的下载中,你将在 ~/Brochures 文件夹中找到七个 PDF 小册子文件,其中一个用于除“海鲜”以外的每个类别。 我故意省略了添加一本《海鲜》小册子,以说明如何处理并非所有记录都有关联的二进制数据的情况。 若要使用这些值更新 Categories 表,请右键单击服务器资源管理器中的 Categories 节点,然后选择“显示表数据”。 然后,输入每个有小册子的类别对应的小册子文件的虚拟路径,如图 1 所示。 由于海鲜类别没有小册子,请将其 BrochurePath 列的值保持为 NULL。
图 1:手动输入表Categories列的值BrochurePath(单击以查看全尺寸图像)
步骤 2:为 GridView 中的小册子提供下载链接
我们已准备好通过为Categories表提供的BrochurePath值,创建一个 GridView,在其中列出每一类及其小册子下载链接。 在步骤 4 中,我们将扩展此 GridView 以显示类别图像。
首先,将 GridView 从工具箱拖到BinaryData文件夹中的DisplayOrDownloadData.aspx页面设计器上。 将 GridView 的 ID 设置为 Categories,并通过 GridView 智能标记选择将其绑定到新的数据源。 具体而言,请将其绑定到一个名为CategoriesDataSource的ObjectDataSource,该数据源使用GetCategories()对象的CategoriesBLL方法检索数据。
图 2:创建名为 CategoriesDataSource 的新 ObjectDataSource (单击可查看全尺寸图像)
图 3:将 ObjectDataSource 配置为使用 CategoriesBLL 类(单击以查看全尺寸图像)
图 4:使用 GetCategories() 方法检索类别列表(单击以查看全尺寸图像)
完成“配置数据源”向导后,Visual Studio 会自动将 BoundField 添加到 Categories GridView 中,以用于 CategoryID,CategoryName,Description,NumberOfProducts 和 BrochurePathDataColumn。 请继续并删除 NumberOfProducts BoundField,因为 GetCategories() 方法的查询无法检索此信息。 此外,请删除 CategoryID BoundField,并将 CategoryName 和 BrochurePath BoundField 的 HeaderText 属性分别重命名为 Category 和 Brochure。 进行这些更改后,GridView 和 ObjectDataSource 声明性标记应如下所示:
<asp:GridView ID="Categories" runat="server"
AutoGenerateColumns="False" DataKeyNames="CategoryID"
DataSourceID="CategoriesDataSource" EnableViewState="False">
<Columns>
<asp:BoundField DataField="CategoryName" HeaderText="Category"
SortExpression="CategoryName" />
<asp:BoundField DataField="Description" HeaderText="Description"
SortExpression="Description" />
<asp:BoundField DataField="BrochurePath" HeaderText="Brochure"
SortExpression="BrochurePath" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
通过浏览器查看此页面(请参阅图 5)。 列出了八个类别中的每一个。 七个包含BrochurePath值的类别在各自的BoundField中显示BrochurePath值。 海鲜对于其BrochurePath价值而言,显示为空的单元格。
图 5:列出了每个类别的名称、说明和 BrochurePath 值(单击以查看全尺寸图像)
我们希望创建一个指向小册子的链接,而不是显示 BrochurePath 列的文本。 为此,请删除 BrochurePath BoundField 并将其替换为 HyperLinkField。 将新的 HyperLinkField 属性 HeaderText 设置为“小册子”,将其 Text 属性设置为“查看小册子”,并将属性 DataNavigateUrlFields 设置为 BrochurePath。
图 6:为BrochurePath添加HyperLinkField
这会将一列链接添加到 GridView,如图 7 所示。 单击“查看手册”链接将直接在浏览器中显示 PDF,或提示用户下载文件,具体取决于是否安装了 PDF 阅读器和浏览器设置。
图 7:可以通过单击“查看小册子”链接来查看类别的小册子(单击以查看全尺寸图像)
图 8:显示该类别的小册子 PDF(单击以查看全尺寸图像)
隐藏不包含小册子的类别中的"查看小册子"文本
如图 7 所示,BrochurePath HyperLinkField 为所有记录显示其 Text 属性值(查看宣传册),无论 BrochurePath 是否有非NULL 值。 当然,如果是BrochurePathNULL,则链接仅显示为文本,与“海鲜”类别的情况一样(请参阅图 7)。 与其显示“查看小册子”文本,不如对于没有 BrochurePath 值的类别显示一些备用文本,例如“无小册子可用”。
为了提供此行为,我们需要使用 TemplateField,该模板字段的内容是通过调用页面方法生成的,该方法会根据 BrochurePath 值发出相应的输出。 我们首先在 GridView 控件教程的 Using TemplateFields 中回顾了此格式设置技术。
通过选择 BrochurePath HyperLinkField,然后单击编辑列对话框中的“将此字段转换为 TemplateField”链接,将 HyperLinkField 转换为 TemplateField。
图 9:将 HyperLinkField 转换为 TemplateField
这将创建一个 TemplateField,包含一个 ItemTemplate HyperLink Web 控件,其 NavigateUrl 属性绑定到 BrochurePath 值。 将此标记替换为对方法 GenerateBrochureLink的调用,并传入以下 BrochurePath值:
<asp:TemplateField HeaderText="Brochure">
<ItemTemplate>
<%# GenerateBrochureLink(Eval("BrochurePath")) %>
</ItemTemplate>
</asp:TemplateField>
接下来,在 ASP.NET 页面的代码隐藏类中,创建一个名为GenerateBrochureLink的方法,该方法返回String并接受Object作为输入参数。
Protected Function GenerateBrochureLink(BrochurePath As Object) As String
If Convert.IsDBNull(BrochurePath) Then
Return "No Brochure Available"
Else
Return String.Format("<a href="{0}">View Brochure</a>", _
ResolveUrl(BrochurePath.ToString()))
End If
End Function
此方法确定传入 Object 值是否为数据库 NULL ,如果是,则返回一条消息,指示该类别缺少小册子。 否则,如果存在 BrochurePath 值,则会在超链接中显示该值。 请注意,如果存在该值 BrochurePath ,则将其传递到 ResolveUrl(url) 方法中。 此方法解析传入的 url,将字符 ~ 替换为相应的虚拟路径。 例如,如果应用程序植根于该位置 /Tutorial55, ResolveUrl("~/Brochures/Meats.pdf") 将返回 /Tutorial55/Brochures/Meat.pdf。
图 10 显示应用这些更改后的页面。 请注意,“海鲜”类别字段 BrochurePath 现在显示“无小册子可用”文本。
图 10:对于没有小册子的类别,将显示“无小册子可用”的文本(单击可查看全尺寸图像)
步骤 3:添加网页以显示类别图片
当用户访问 ASP.NET 页面时,他们会收到 ASP.NET 页的 HTML。 收到的 HTML 只是文本,不包含任何二进制数据。 任何其他二进制数据(如图像、声音文件、宏媒体 Flash 应用程序、嵌入式Windows 媒体播放器视频等)都作为 Web 服务器上的单独资源存在。 HTML 包含对这些文件的引用,但不包括文件的实际内容。
例如,在 HTML <img> 中,元素用于引用图片,该 src 属性指向图像文件,如下所示:
<img src="MyPicture.jpg" ... />
当浏览器收到此 HTML 时,它会向 Web 服务器发出另一个请求,以检索图像文件的二进制内容,然后在浏览器中显示该文件。 相同的概念适用于任何二进制数据。 在步骤 2 中,该小册子未作为页面 HTML 标记的一部分发送到浏览器。 相反,呈现的 HTML 提供了超链接,单击时,浏览器直接请求 PDF 文档。
若要显示或允许用户下载驻留在数据库中的二进制数据,我们需要创建返回数据的单独网页。 对于应用程序,只有一个二进制数据字段直接存储在类别图片的数据库中。 因此,我们需要一个页面,在调用时返回特定类别的图像数据。
向名为 BinaryData 的文件夹添加新的 ASP.NET 页DisplayCategoryPicture.aspx。 执行此操作时,请取消选中“选择母版页”复选框。 此页面需要 CategoryID 查询字符串中的值,并返回该类别的 Picture 列的二进制数据。 由于此页面返回二进制数据,而没有任何其他数据,因此它不需要 HTML 节中的任何标记。 因此,单击左下角的“源”选项卡,删除除指令以外的 <%@ Page %> 所有页面标记。 也就是说, DisplayCategoryPicture.aspx 声明性标记应包含一行:
<%@ Page Language="VB" AutoEventWireup="true"
CodeFile="DisplayCategoryPicture.aspx.vb"
Inherits="BinaryData_DisplayCategoryPicture" %>
如果在<%@ Page %>指令中看到MasterPageFile属性,请将其删除。
在页面代码隐藏类中,将以下代码添加到 Page_Load 事件处理程序:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim categoryID As Integer = _
Convert.ToInt32(Request.QueryString("CategoryID"))
' Get information about the specified category
Dim categoryAPI As New CategoriesBLL()
Dim categories As Northwind.CategoriesDataTable = _
categoryAPI.GetCategoryWithBinaryDataByCategoryID(categoryID)
Dim category As Northwind.CategoriesRow = categories(0)
' Output HTTP headers providing information about the binary data
Response.ContentType = "image/bmp"
' Output the binary data
' But first we need to strip out the OLE header
Const OleHeaderLength As Integer = 78
Dim strippedImageLength As Integer = _
category.Picture.Length - OleHeaderLength
Dim strippedImageData(strippedImageLength) As Byte
Array.Copy(category.Picture, OleHeaderLength, _
strippedImageData, 0, strippedImageLength)
Response.BinaryWrite(strippedImageData)
End Sub
此代码首先将 CategoryID 查询字符串的值读取到一个名为 categoryID 的变量中。 接下来,通过调用 CategoriesBLL 类的方法 GetCategoryWithBinaryDataByCategoryID(categoryID) 检索图片数据。 此数据通过使用 Response.BinaryWrite(data) 方法返回到客户端,但在调用此方法之前,必须删除 Picture 列值的 OLE 标头。 这是通过创建一个名为BytestrippedImageData的数组来实现的,该数组的字符数比Picture列少正好78个字符。
该方法Array.Copy用于将数据从category.Picture位置 78 开始复制到strippedImageData。
该 Response.ContentType 属性指定返回的内容 的 MIME 类型 ,以便浏览器知道如何呈现它。
Categories由于表的Picture列是位图图像,因此此处使用了位图 MIME 类型(image/bmp)。 如果省略 MIME 类型,大多数浏览器仍会正确显示图像,因为它们可以根据图像文件二进制数据的内容推断该类型。 因此,尽可能包括 MIME 类型是明智的。 有关 MIME 媒体类型的完整列表,请参阅 Internet 分配号码机构的网站。
创建此页面后,可以通过访问 DisplayCategoryPicture.aspx?CategoryID=categoryID来查看特定类别的图片。 图 11 显示了“饮料”类别的图片,可从中查看 DisplayCategoryPicture.aspx?CategoryID=1。
图 11:显示饮料类别图片(单击以查看全尺寸图像)
如果在访问 DisplayCategoryPicture.aspx?CategoryID=categoryID 时收到异常消息“无法将类型为 'System.DBNull' 的对象转换为类型为 'System.Byte[]'”,可能是由两个原因导致的。 首先, Categories 表的 Picture 列允许 NULL 值。 但是,页面 DisplayCategoryPicture.aspx 假定存在非NULL 值。 如果Picture属性具有NULL值,则无法直接访问CategoriesDataTable。 如果确实要允许 Picture 列中的 NULL 值,需要包括以下条件:
If category.IsPictureNull() Then
' Display some "No Image Available" picture
Response.Redirect("~/Images/NoPictureAvailable.gif")
Else
' Send back the binary contents of the Picture column
' ... Set ContentType property and write out ...
' ... data via Response.BinaryWrite ...
End If
上面的代码假定在Images文件夹中有一个名为NoPictureAvailable.gif的图像文件,你希望在没有图片的类别中显示它。
如果 CategoriesTableAdapter s GetCategoryWithBinaryDataByCategoryID 方法的 SELECT 语句已还原回主查询的列列表,则也可能引发此异常,如果使用的是即席 SQL 语句,并且重新运行 TableAdapter 主查询的向导,则可能会发生这种情况。 检查并确保 GetCategoryWithBinaryDataByCategoryID 方法的 SELECT 语句仍然包含该 Picture 列。
注意
每次访问DisplayCategoryPicture.aspx时,都会访问数据库,并返回指定类别的图片数据。 但是,如果用户上次查看该类别后图片没有更改,则这浪费了工作量。 幸运的是,HTTP 允许 条件 GET。 使用条件 GET 时,发出 HTTP 请求的客户端会随 If-Modified-Since HTTP 标头 发送,该标头提供客户端上次从 Web 服务器检索到此资源的日期和时间。 如果内容自此指定日期以来未更改,Web 服务器可能会响应 “未修改”状态代码(304), 并放弃发送回请求的资源内容。 简言之,如果自客户端上次访问资源以来尚未修改该资源的内容,则此方法可缓解 Web 服务器不得不发送回内容。
若要实现此行为,需要在Categories表中添加一个PictureLastModified列,以记录Picture列上次更新的时间,并添加用于检查If-Modified-Since标头的代码。 如果想了解有关 If-Modified-Since 头信息和条件 GET 工作流的更多信息,请参阅 适用于 RSS 黑客的 HTTP 条件 GET 和 深入了解在 ASP.NET 页面中执行 HTTP 请求。
步骤 4:在 GridView 中显示类别图片
现在,我们有一个网页来显示特定类别的图片,可以使用图像 Web 控件或指向<img>的 HTML DisplayCategoryPicture.aspx?CategoryID=categoryID 元素来显示它。 其 URL 由数据库数据确定的图像可以使用 ImageField 显示在 GridView 或 DetailsView 中。 ImageField 包含DataImageUrlField和DataImageUrlFormatString属性,其工作类似于 HyperLinkField 和DataNavigateUrlFieldsDataNavigateUrlFormatString属性。
让我们通过添加 ImageField 来增强 Categories GridView DisplayOrDownloadData.aspx ,以显示每个类别的图片。 只需添加 ImageField 并将其DataImageUrlField属性设置为CategoryID,DataImageUrlFormatString属性设置为DisplayCategoryPicture.aspx?CategoryID={0}。 这将创建一个 GridView 列,该列呈现一个元素<img>,其中属性src引用DisplayCategoryPicture.aspx?CategoryID={0},并且{0}会被 GridView 行的CategoryID值替换。
图 12:将 ImageField 添加到 GridView
添加 ImageField 后,GridView 的声明性语法应如下所示:
<asp:GridView ID="Categories" runat="server" AutoGenerateColumns="False"
DataKeyNames="CategoryID" DataSourceID="CategoriesDataSource"
EnableViewState="False">
<Columns>
<asp:BoundField DataField="CategoryName" HeaderText="Category"
SortExpression="CategoryName" />
<asp:BoundField DataField="Description" HeaderText="Description"
SortExpression="Description" />
<asp:TemplateField HeaderText="Brochure">
<ItemTemplate>
<%# GenerateBrochureLink(Eval("BrochurePath")) %>
</ItemTemplate>
</asp:TemplateField>
<asp:ImageField DataImageUrlField="CategoryID"
DataImageUrlFormatString="DisplayCategoryPicture.aspx?CategoryID={0}">
</asp:ImageField>
</Columns>
</asp:GridView>
花点时间通过浏览器查看此页面。 请注意现在每个记录为类别包含了一张图片。
图 13:每行显示类别图片(单击以查看全尺寸图像)
总结
本教程介绍了如何呈现二进制数据。 数据呈现方式取决于数据类型。 对于 PDF 小册子文件,我们为用户提供了一个“查看小册子”链接,单击该链接会将用户直接转到 PDF 文件。 对于类别图片,我们首先创建了一个页面来检索和返回数据库中的二进制数据,然后使用该页在 GridView 中显示每个类别的图片。
现在,我们已经了解了如何显示二进制数据,我们准备检查如何使用二进制数据对数据库执行插入、更新和删除操作。 在下一教程中,我们将了解如何将上传的文件与其相应的数据库记录相关联。 在本教程之后,我们将了解如何更新现有二进制数据,以及如何在删除其关联的记录时删除二进制数据。
快乐编程!
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams自学ASP.NET 2.0 24小时教程》。 可以通过mitchell@4GuysFromRolla.com联系他。
特别感谢
本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy 和 Dave Gardner。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我发个信息。mitchell@4GuysFromRolla.com