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

作者 :Scott Mitchell

下载 PDF

在本教程中,我们将介绍在网页上显示二进制数据的选项,包括图像文件的显示和为 PDF 文件提供“下载”链接。

简介

在前面的教程中,我们探讨了将二进制数据与应用程序基础数据模型关联的两种技术,并使用 FileUpload 控件将文件从浏览器上传到 Web 服务器的文件系统。 我们尚未了解如何将上传的二进制数据与数据模型相关联。 也就是说,上传文件并将其保存到文件系统后,文件的路径必须存储在相应的数据库记录中。 如果数据直接存储在数据库中,则上传的二进制数据无需保存到文件系统,但必须注入数据库。

不过,在了解如何将数据与数据模型关联之前,让我们先看看如何向最终用户提供二进制数据。 呈现文本数据非常简单,但应如何呈现二进制数据? 当然,这取决于二进制数据的类型。 对于图像,我们可能需要显示图像;对于 PDF、Microsoft Word文档、ZIP 文件和其他类型的二进制数据,提供下载链接可能更合适。

在本教程中,我们将了解如何使用数据 Web 控件(如 GridView 和 DetailsView)显示二进制数据及其关联的文本数据。 在下一教程中,我们将把注意力转向将上传的文件与数据库相关联。

步骤 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.aspxDesigner。 将 GridView ID 设置为 Categories ,并通过 GridView 智能标记选择将其绑定到新的数据源。 具体而言,请将其绑定到名为 CategoriesDataSource 的 ObjectDataSource,该对象使用 CategoriesBLL 对象 s GetCategories() 方法检索数据。

创建新的对象DataSource 命名类别DataSource

图 2:新建名为 CategoriesDataSource 的对象数据源 (单击以查看全尺寸图像)

将 ObjectDataSource 配置为使用 CategoriesBLL 类

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

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

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

完成“配置数据源”向导后,Visual Studio 会自动将 BoundField 添加到 Categories GridView CategoryID中的 、 CategoryNameDescriptionNumberOfProductsBrochurePathDataColumn 。 继续删除 BoundField,NumberOfProductsGetCategories()因为方法查询不会检索此信息。 另请删除 CategoryID BoundField,并将 CategoryNameBrochurePath 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 控件中使用 TemplateFields 教程中 探索了这种格式设置技术。

通过选择 BrochurePath HyperLinkField,然后单击“编辑列”对话框中的“将此字段转换为 TemplateField”链接,将 HyperLinkField 转换为 TemplateField。

将 HyperLinkField 转换为 TemplateField

图 9:将 HyperLinkField 转换为 TemplateField

这将创建一个 TemplateField,其中包含ItemTemplate其属性绑定到 BrochurePath 值的 HyperLink Web 控件NavigateUrl。 将此标记替换为对 方法 GenerateBrochureLink的调用 ,并传入 的值 BrochurePath

<asp:TemplateField HeaderText="Brochure">
    <ItemTemplate>
        <%# GenerateBrochureLink(Eval("BrochurePath")) %>
    </ItemTemplate>
</asp:TemplateField>

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

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 只是文本,不包含任何二进制数据。 任何其他二进制数据(如图像、声音文件、Macromedia Flash应用程序、嵌入式Windows 媒体播放器视频等)都作为单独的资源存在于 Web 服务器上。 HTML 包含对这些文件的引用,但不包括文件的实际内容。

例如,在 HTML 中, <img> 元素用于引用图片,其 src 属性指向图像文件,如下所示:

<img src="MyPicture.jpg" ... />

当浏览器收到此 HTML 时,它会向 Web 服务器发出另一个请求,以检索图像文件的二进制内容,然后它将显示在浏览器中。 相同的概念适用于任何二进制数据。 在步骤 2 中,没有将小册子作为页面 HTML 标记的一部分发送到浏览器。 相反,呈现的 HTML 提供的超链接在单击时会导致浏览器直接请求 PDF 文档。

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

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

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

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

在页面代码隐藏类中,将以下代码添加到 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 类 s GetCategoryWithBinaryDataByCategoryID(categoryID) 方法检索图片数据。 此数据通过使用 Response.BinaryWrite(data) 方法返回到客户端,但在调用它之前, Picture 必须删除列值 OLE 标头。 这是通过创建名为 bytestrippedImageData 数组来实现的,该数组包含的字符数比 Picture 列中的字符少 78 个字符。 方法Array.Copy用于将数据从category.Picture位置 78 strippedImageData开始复制到 。

属性 Response.ContentType 指定要返回的内容的 MIME 类型 ,以便浏览器知道如何呈现它。 Categories由于表 s 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 表 s 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 方法 s SELECT 语句已还原到main查询列列表,则也可能导致此异常。如果使用临时 SQL 语句,并且已为 TableAdapter main 查询重新运行向导,则可能会出现此异常。 检查以确保 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 ,请参阅 RSS 黑客的 HTTP 条件 GET深入了解在 ASP.NET 页面中执行 HTTP 请求

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

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

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

将 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 中显示每个类别的图片。

现在我们已经了解了如何显示二进制数据,接下来我们准备检查如何使用二进制数据对数据库执行插入、更新和删除操作。 在下一教程中,我们将了解如何将上传的文件与其相应的数据库记录相关联。 在此之后的教程中,我们将了解如何更新现有二进制数据,以及如何在删除二进制数据关联的记录时删除二进制数据。

编程愉快!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。

特别感谢

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