显示数据 Web 控件中的二进制数据 (C#)

作者 :斯科特·米切尔

下载 PDF

在本教程中,我们将查看在网页上显示二进制数据的选项,包括图像文件的显示以及为 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 标头与等效的 JPG 图像一起使用,而无需不必要的 OLE 标头。

在前面的教程中,我们了解了如何使用 FileUpload 控件。 因此,可以继续将小册子文件添加到 Web 服务器文件系统。 但是,这样做不会更新表中的BrochurePathCategories列。 在下一个教程中,我们将了解如何完成此操作,但现在我们需要手动为此列提供值。

在本教程的下载中,你将在 ~/Brochures 文件夹中找到七个 PDF 小册子文件,其中一个用于除“海鲜”以外的每个类别。 我故意省略了添加一本《海鲜》小册子,以说明如何处理并非所有记录都有关联的二进制数据的情况。 若要使用这些值更新 Categories 表,请右键单击服务器资源管理器中的 Categories 节点,然后选择“显示表数据”。 然后,输入具有小册子的每个类别的小册子文件的虚拟路径,如图 1 所示。 由于海鲜类别没有小册子,因此将其 BrochurePath 列的值保留为 NULL

手动输入类别表的“小册子路径”列的值

图 1:手动输入表BrochurePath列的值Categories单击以查看全尺寸图像

BrochurePath通过为Categories表提供的值,我们准备创建一个 GridView,其中列出了每个类别以及下载类别小册子的链接。 在步骤 4 中,我们将扩展此 GridView 以显示类别图像。

首先,将 GridView 从工具箱拖到文件夹中页面BinaryDataDisplayOrDownloadData.aspx设计器上。 将 GridView 设置为 ID Categories GridView 智能标记以及通过 GridView 智能标记,选择将其绑定到新的数据源。 具体而言,请将其绑定到一个名为CategoriesDataSource使用对象方法GetCategories()检索数据的 CategoriesBLL ObjectDataSource。

创建名为 CategoriesDataSource 的新 ObjectDataSource

图 2:创建名为 CategoriesDataSource 的新 ObjectDataSource (单击可查看全尺寸图像

将 ObjectDataSource 配置为使用 CategoriesBLL 类

图 3:将 ObjectDataSource 配置为使用 CategoriesBLL 类(单击以查看全尺寸图像

使用 GetCategories() 方法检索类别列表

图 4:使用 GetCategories() 方法检索类别列表(单击以查看全尺寸图像

完成“配置数据源”向导后,Visual Studio 会自动将 BoundField 添加到 Categories GridView for the CategoryID, , CategoryNameNumberOfProductsDescriptionBrochurePath DataColumn s。 继续操作并删除 NumberOfProducts BoundField, GetCategories() 因为方法的查询不会检索此信息。 此外, CategoryID 请删除 BoundField 并将 CategoryName BrochurePath and BoundFields 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 值的七个类别具有 BrochurePath 相应的 BoundField 中显示的值。 NULL海鲜具有其BrochurePath价值,它显示一个空的单元格。

列出了每个类别的名称、说明和小册子路径值

图 5:列出了每个类别的名称、说明和 BrochurePath 值(单击以查看全尺寸图像

我们希望创建一个指向小册子的链接,而不是显示该列的文本 BrochurePath 。 为此,请删除 BrochurePath BoundField 并将其替换为 HyperLinkField。 将新的 HyperLinkField 属性 HeaderText 设置为“小册子”,将其 Text 属性设置为“查看小册子”,并将属性 DataNavigateUrlFields 设置为 BrochurePath

为 BrochurePath 添加 HyperLinkField

图 6:为 /> 添加 HyperLinkField BrochurePath

这会将一列链接添加到 GridView,如图 7 所示。 单击“查看手册”链接将直接在浏览器中显示 PDF,或提示用户下载文件,具体取决于是否安装了 PDF 阅读器和浏览器设置。

可以通过单击“查看小册子”链接查看类别的小册子

图 7:可以通过单击“查看小册子”链接来查看类别的小册子(单击以查看全尺寸图像

显示类别的小册子 PDF

图 8:显示类别的小册子 PDF(单击以查看全尺寸图像

隐藏没有小册子的类别的视图小册子文本

如图 7 所示, BrochurePath HyperLinkField 为所有记录显示其 Text 属性值(查看小册子),而不考虑是否有非NULLBrochurePath。 当然,如果是BrochurePathNULL,则链接仅显示为文本,与“海鲜”类别的情况一样(请参阅图 7)。 与其显示文本“查看小册子”,最好让这些类别没有 BrochurePath 值显示一些备用文本,例如“无小册子可用”。

为了提供此行为,我们需要使用 TemplateField,该模板字段的内容是通过调用页面方法生成的,该方法会根据 BrochurePath 值发出相应的输出。 我们首先在 GridView 控件教程的 Using TemplateFields 中回顾了此格式设置技术。

通过选择 BrochurePath HyperLinkField,然后单击“编辑列”对话框中的“将此字段转换为 TemplateField”链接,将 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>

接下来,在名为返回并接受object输入参数的 ASP.NET 页代码隐藏类stringGenerateBrochureLink中创建protected方法。

protected string GenerateBrochureLink(object BrochurePath)
{
    if (Convert.IsDBNull(BrochurePath))
        return "No Brochure Available";
    else
        return string.Format(@"<a href="{0}">View Brochure</a>", 
            ResolveUrl(BrochurePath.ToString()));
}

此方法确定传入 object 值是否为数据库 NULL ,如果是,则返回一条消息,指示该类别缺少小册子。 否则,如果存在 BrochurePath 值,则会在超链接中显示该值。 请注意,如果存在该值 BrochurePath ,则将其传递到 ResolveUrl(url) 方法中。 此方法解析传入 的 URL,将 ~ 字符替换为相应的虚拟路径。 例如,如果应用程序植根于该位置 /Tutorial55ResolveUrl("~/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 文档。

若要显示或允许用户下载驻留在数据库中的二进制数据,我们需要创建返回数据的单独网页。 对于应用程序,只有一个二进制数据字段直接存储在类别图片的数据库中。 因此,我们需要一个页面,在调用时返回特定类别的图像数据。

向名为 DisplayCategoryPicture.aspx 的文件夹添加新的 ASP.NET 页BinaryData。 执行此操作时,请取消选中“选择母版页”复选框。 此页面需要 CategoryID 查询字符串中的值,并返回该类别的 Picture 列的二进制数据。 由于此页面返回二进制数据,而没有任何其他数据,因此它不需要 HTML 节中的任何标记。 因此,单击左下角的“源”选项卡,删除除指令以外的 <%@ Page %> 所有页面标记。 也就是说, DisplayCategoryPicture.aspx 声明性标记应包含一行:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="DisplayCategoryPicture.aspx.cs" 
    Inherits="BinaryData_DisplayCategoryPicture" %>

如果在指令中看到<%@ Page %>MasterPageFile属性,请将其删除。

在页面代码隐藏类中,将以下代码添加到 Page_Load 事件处理程序:

protected void Page_Load(object sender, EventArgs e)
{
    int categoryID = Convert.ToInt32(Request.QueryString["CategoryID"]);
    // Get information about the specified category
    CategoriesBLL categoryAPI = new CategoriesBLL();
    Northwind.CategoriesDataTable categories = 
        categoryAPI.GetCategoryWithBinaryDataByCategoryID(categoryID);
    Northwind.CategoriesRow category = 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 int OleHeaderLength = 78;
    int strippedImageLength = category.Picture.Length - OleHeaderLength;
    byte[] strippedImageData = new byte[strippedImageLength];
    Array.Copy(category.Picture, OleHeaderLength, 
        strippedImageData, 0, strippedImageLength);
    
    Response.BinaryWrite(strippedImageData);
}

此代码首先在 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。 如果确实要允许 NULL 列的值 Picture ,需要包括以下条件:

if (category.IsPictureNull())
{
    // 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 ...
}

上面的代码假定文件夹中有一些名称的图像NoPictureAvailable.gifImages文件,你希望在没有图片的情况下显示这些类别。

如果 CategoriesTableAdapter s GetCategoryWithBinaryDataByCategoryID 方法的 SELECT 语句已还原回主查询的列列表,则也可能引发此异常,如果使用的是即席 SQL 语句,并且重新运行 TableAdapter 主查询的向导,则可能会发生这种情况。 检查以确保 GetCategoryWithBinaryDataByCategoryID 方法的 SELECT 语句仍然包含该 Picture 列。

注意

每次访问数据库时 DisplayCategoryPicture.aspx ,都会访问数据库,并返回指定的类别图片数据。 但是,如果用户上次查看该类别后图片没有更改,则这浪费了工作量。 幸运的是,HTTP 允许 条件 GET。 使用条件 GET 时,发出 HTTP 请求的客户端会随 If-Modified-Since HTTP 标头发送,该标头 提供客户端上次从 Web 服务器检索到此资源的日期和时间。 如果内容自此指定日期以来未更改,Web 服务器可能会响应 “未修改”状态代码(304), 并放弃发送回请求的资源内容。 简言之,如果自客户端上次访问资源以来尚未修改该资源的内容,则此方法可缓解 Web 服务器不得不发送回内容。

但是,若要实现此行为,需要将列添加到PictureLastModifiedCategories表中以捕获上次更新列的时间Picture,以及检查If-Modified-Since标头的代码。 有关标头和条件 GET 工作流的详细信息 If-Modified-Since ,请参阅 HTTP 条件 GET for RSS 黑客 ,以及 更深入地了解在 ASP.NET 页面中执行 HTTP 请求。

步骤 4:在 GridView 中显示类别图片

现在,我们有一个网页来显示特定类别的图片,可以使用图像 Web 控件或指向DisplayCategoryPicture.aspx?CategoryID=categoryID的 HTML <img> 元素来显示它 其 URL 由数据库数据确定的图像可以使用 ImageField 显示在 GridView 或 DetailsView 中。 ImageField 包含DataImageUrlFieldDataImageUrlFormatString属性,其工作类似于 HyperLinkField 和DataNavigateUrlFieldsDataNavigateUrlFormatString属性。

让我们通过添加 ImageField 来增强 Categories GridView DisplayOrDownloadData.aspx ,以显示每个类别的图片。 只需添加 ImageField 并将其及其属性分别设置为DataImageUrlFieldCategoryID和设置DisplayCategoryPicture.aspx?CategoryID={0}DataImageUrlFormatString 这将创建一个 GridView 列,该列呈现<img>其属性引用DisplayCategoryPicture.aspx?CategoryID={0}的元素,其中{0}替换为 GridView 行的值CategoryIDsrc

将 ImageField 添加到 GridView

图 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 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0。 他可以通过他的博客联系到mitchell@4GuysFromRolla.com他,可以在该博客中找到http://ScottOnWriting.NET

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy 和 Dave Gardner。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请把我扔一条线。mitchell@4GuysFromRolla.com