在 Web 数据控件中显示二进制数据54

简介

前面的教程中 , 我们介绍了将二进制数据与应用程序的基础数据模型相关联的两种方法 , 并使用FileUpload 控件从浏览器向 Web 服务器的文件系统上载。我们还将了解怎样将上载的二进制数据与数据模型相关联。即,将文件上载并保存到文件系统之后,文件的路径必须存储在相应的数据库记录中。如果数据直接存储在数据库中,则上载的二进制数据不需要保存到文件系统,但是必须注入到数据库中。

在介绍将数据和数据模型相关联之前 , 我们首先介绍如何向最终用户呈现二进制数据。呈现文本数据非常简单,但是应如何呈现二进制数据呢?当然,这取决于二进制数据的类型。对于图像,最希望的是显示图像;对于 PDF 、 Microsoft Word 文档、 ZIP 文件和其它类型的二进制数据,提供“下载”链接可能更合适。

本教程中,我们将了解怎样使用 Web 数据控件(如 GridView 和 DetailsView )将二进制数据呈现在与其相关联的文本数据旁。下一教程中,我们将把注意力转到将上载的文件与数据库相关联方面。

步骤1:提供BrochurePath 的值

Categories 表中的 Picture 列已经包含各种类别图像的二进制数据。具体地说,每条记录的 Picture 列保存粗颗粒的、低质的 16 色位图图像的二进制内容。每种类别的图像都是 172 像素宽和 120 像素高,占用大约 11 KB 空间。而且, Picture 列中的二进制内容包含 78 字节的 OLE 标头,显示图像之前必须将其去除。此标头信息的存在是因为Northwind 数据库源于 Microsoft Access 。在Access 中 , 使用 OLE Object 数据类型存储二进制数据 , 该类型会添加标头。现在,我们将了解怎样从这些低质的图像中去除标头以便显示图片。在后面的教程中,我们将构建一个界面用于更新类别的 Picture 列,并使用等效的不包含不必要的 OLE 标头的 JPG 图像替换这些使用 OLE 标头的位图图像。

在前面的教程中,我们介绍了如何使用 FileUpload 控件。因此,您可以往下进行并向 Web 服务器的文件系统添加小册子的文件。但是,这样做并没有更新 Categories 表中的 BrochurePath 列。下一教程中,我们将了解怎样实现更新,但是现在我们需要手工为此列提供值。

在本教程的下载版中, 您将在 ~/Brochures 文件夹中看到 7 个 PDF 小册子的文件 , 除 Seafood 之外的每种类别都有一个小册子。我故意没有添加 Seafood 小册子是为了说明在不是所有记录都有相关联的二进制数据的情况下,我们该如何处理。要使用这些值更新Categories 表 , 可以从服务器资源管理器上右键单击 Categories 节点 , 然后选择Show Table Data 。然后,为拥有小册子的每种类别的小册子文件输入虚拟路径,如图 1 所示。由于 Seafood 类别没有小册子,将其 BrochurePath 列的值保留为 NULL 。

图1 : 手工为 Categories 表的 BrochurePath 列输入值

步骤2 : 在GridView 中为小册子提供下载链接

为Categories 表提供 BrochurePath 值之后 , 我们就可以创建能列出每种类别和该类别的小册的下载链接的GridView 了 。在第 4 步,我们将扩展此 GridView ,使其还可以显示类别的图像。

首先 , 将一个GridView 从工具箱拖放到 BinaryData 文件夹中DisplayOrDownloadData.aspx 页的设计器上。将 GridView 的 ID 设置为 Categories , 并从 GridView 的智能标记中 , 选择将它绑定到新的数据源。具体来说 , 将它绑定到名称为 CategoriesDataSource 的 ObjectDataSource ,ObjectDataSource 使用 CategoriesBLL 对象的 GetCategories() 方法来检索数据。

图2 : 创建一个新的名为 CategoriesDataSource 的 ObjectDataSource

图3 : 将 ObjectDataSource 配置为使用 CategoriesBLL 类

图4 :使用 GetCategories() 方法来检索类别的列表

完成 Configure Data Source 向导之后 ,Visual Studio 将自动为 CategoryID 、CategoryName 、Description 、NumberOfProducts 和 BrochurePath DataColumns 向 Categories GridView 添加 BoundField 。继续往下进行 , 删除 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 为 NULL 值的 Seafood 显示空单元。

图5 : 列出每种类别的 Name 、Description 和BrochurePath 的值

我们需要的不是显示 BrochurePath 列的文本 , 而是要创建到小册子的链接。要实现这个目标 , 需要删除 BrochurePath BoundField 并用 HyperLinkField 来替换它。将新的 HyperLinkField 的 HeaderText 属性设置为 “Brochure” ,Text 属性设置为 “View Brochure” ,DataNavigateUrlFields 属性设置为BrochurePath 。

图6 :为 BrochurePath 添加一个 HyperLinkField

这将向GridView 添加一列链接 , 如图 7 所示。单击 “View Brochure” 链接将会直接在浏览器中显示 PDF 或提示用户下载该文件,具体取决于是否安装了 PDF 阅读器以及浏览器的设置。

图7 : 单击 “View Brochure” 链接可以查看某种类别的小册子

图8 : 显示某个类别的 PDF 小册子

隐藏没有小册子的类别的“View Brochure” 文本

如图7 所示 ,BrochurePath HyperLinkField 对所有记录都显示它的 Text 属性值 (“View Brochure”) , 而不管 BrochurePath 是否为非 NULL 值。当然,如果 BrochurePath 为 NULL ,则链接仅显示为文本,就像 Seafood 类别显示的一样(请参阅图 7 )。与显示文本 “View Brochure” 相比 , 让那些没有 BrochurePath 值的类别显示一些替代文本 ( 如“No Brochure Available.” ) 可能更好。

为此目的 , 我们需要使用TemplateField , 它的内容通过调用一个页面方法来生成 , 该方法根据BrochurePath 的值给出相应的输出。我们在前面的 在 GridView控件中使用 TemplateField 教程中首先介绍了这种格式化方法。

通过选择BrochurePath HyperLinkField , 然后在 Edit Columns 对话框中单击“Convert this field into a TemplateField” 链接 , 可以将HyperLinkField 转变为 TemplateField 。

图9 : 将 HyperLinkField 转换为 TemplateField

这将创建一个带有 ItemTemplate 的 TemplateField , 它包含一个其NavigateUrl 属性绑定到 BrochurePath 的值的 HyperLink Web 控件。通过调用方法 GenerateBrochureLink 并传入BrochurePath 的值来替换此标记 :

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

接下来 , 在ASP.NET 页的代码文件类中创建一个名为GenerateBrochureLink 的 受保护方法 , 它返回一个字符串并接受一个对象作为输入参数。

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())); 
}

此方法判定传入的对象值是否为数据库NULL 值 , 如果是 , 返回一条指示此类别没有小册子的消息。否则,如果有 BrochurePath 值,则将该值显示在超链接中。请注意 , 如果存在BrochurePath 值 , 该值也将传递给 ResolveUrl(url) 方法 。此方法解析传入的 url ,将 ~ 字符替换为相应的虚拟路径。例如 , 如果应用程序的根路径为/Tutorial55 ,ResolveUrl("~/Brochures/Meats.pdf") 将返回 /Tutorial55/Brochures/Meat.pdf 。

图 10 显示应用这些更改之后的页面。请注意,现在 Seafood 类别的 BrochurePath 字段显示文本 “No Brochure Available” 。

图10 : 没有小册子的类别显示文本 “No Brochure Available”

步骤3 : 添加一个网页来显示类别的图片

用户访问ASP.NET 网页时 , 他们接收 ASP.NET 网页的 HTML 。接收的 HTML 只是文本,并不包含任何二进制数据。任何附加的二进制数据 , 如图像、声音文件、Macromedia Flash 应用、嵌入的 Windows Media Player 视频等 , 都作为独立的资源存在于Web 服务器上。 HTML 包含对这些文件的引用,但不包含文件的实际内容。

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

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

浏览器收到此HTML 后 , 向 Web 服务器进行另一次请求来检索图像的二进制内容 , 然后在浏览器中显示该图像。同样的概念适用于任何二进制数据。在第 2 步中,没有将小册子作为页面的 HTML 标记的一部分发送给浏览器。而是呈现的 HTML 提供了超链接,单击链接时,使浏览器直接请求PDF 文档。

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

向 BinaryData 文件夹添加一个名称为DisplayCategoryPicture.aspx 的ASP.NET 页。添加时 , 保留 “Select master page” 复选框为未选中。此页的查询字符串中需要一个CategoryID 值,并返回该类别Picture 列的二进制数据。由于此页只返回二进制数据,其他什么都不返回,因此在 HTML 部分不需要任何标记。因此,单击左下角的 Source 选项卡,删除除 <%@ Page %> 指令之外的所有页面的标记。即 ,DisplayCategoryPicture.aspx 的声明标记应该由一行组成 :

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

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

在页面的代码文件类中 , 将以下代码添加到 Page_Load Event Handler :

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 标头。这可以通过以下方法实现:创建一个名称为 strippedImageData 的字节数组,使该字节数组可以保存的字符数刚好比 Picture 列的字符少 78 个。使用 Array.Copy 方法 将数据从 category.Picture 复制到 strippedImageData ,复制时从 78 字节开始 。

Response.ContentType 属性指定返回的内容的 MIME 类型 , 以便浏览器知道如何呈现它。由于 Categories 表的 Picture 列是位图图像 , 因此这里使用位图 MIME 类型 (image/bmp) 。如果省略 MIME 类型,大多数浏览器仍可以正确地显示图像,因为它们可以根据图像文件的二进制数据内容来推断类型。但是,在可能的情况下应尽量指定 MIME 类型。有关 MIME 媒体类型 的完整列表 , 请参阅 Internet Assigned Numbers Authority 的网站 。

创建此页之后 ,就可以 通过访问 DisplayCategoryPicture.aspx?CategoryID=categoryID 查看特定类别的图片了。图 11 显示的是 Beverages 类别的图片,这是访问 DisplayCategoryPicture.aspx?CategoryID=1 查看的。

图11 : 显示 Beverages 类别的图片

如果访问DisplayCategoryPicture.aspx?CategoryID=categoryID 时 , 出现一个显示为 “Unable to cast object of type 'System.DBNull' to type 'System.Byte[]'” 的异常 , 有两种原因可能导致出现这种情况。首先 ,Categories 表的 Picture 列允许 NULL 值。但是DisplayCategoryPicture.aspx 页假定此处提供的是非NULL 值。如果 CategoriesDataTable 的 Picture 属性的值为 NULL , 则不能对其直接访问。 如果确实想允许Picture 列的值为 NULL , 需要包含以下代码 :

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 ... 
}

上述代码假定在 Images 文件夹中有一名称为 NoPictureAvailable.gif 的图像文件 , 对于那些没有图片的类别则显示此图像。

其次 , 如果CategoriesTableAdapter 的GetCategoryWithBinaryDataByCategoryID 方法的SELECT 语句已经还原回主查询的列列表也可以引发此异常 , 如果您正使用 ad-hoc SQL 语句并且已经重新运行TableAdapter 的主查询向导就可能发生这种情况。进行检查以确保GetCategoryWithBinaryDataByCategoryID 方法的SELECT 语句仍包含 Picture 列。

注意 : 每次访问 DisplayCategoryPicture.aspx , 都会访问数据库并返回指定类别的图片数据。如果从用户上次查看类别的图片以后,它未曾改变,则每次都对数据库进行访问就是多余的。幸运的是 ,HTTP 允许使用条件GET 。使用条件 GET , 发出 HTTP 请求的客户端发送一个 If-Modified-Since HTTP 标头 ,它 提供客户端上次从 Web 服务器检索此资源的日期和时间。如果该日期以后内容没有改变 ,Web 服务器可以用 Not Modified status code (304) 来进行响应 , 并且不往回传送请求的资源内容。总之,如果上次从客户端访问资源以来内容没有改变,此方法可以使 Web 服务器避免往回传送资源的内容。

但是 , 要实现此目标 , 需要向Categories 表添加 PictureLastModified 列 , 以捕获Picture 列上次更新的时间 , 还要添加检查 If-Modified-Since 标头的代码。有关If-Modified-Since 标头和条件 GET 工作流的详细信息 , 请参阅 HTTP Conditional GET for RSS Hackers 和 在 ASP.NET 页中执行 HTTP 请求的深入分析 。

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

现在已经有了显示特定类别图片的网页 , 我们可以使用 Web 图像控件 或指向 DisplayCategoryPicture.aspx?CategoryID=categoryID 的 HTML <img> 元素来显示图片了。其URL 由数据库的数据确定的图像可以使用 ImageField 在 GridView 或 DetailsView 中显示。ImageField 包含 DataImageUrlField 和 DataImageUrlFormatString 属性 , 它们与 HyperLinkField 的DataNavigateUrlFields 和DataNavigateUrlFormatString 属性用法类似。

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

图12 : 向 GridView 添加 ImageField

添加 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 小册子文件,我们为用户提供一个 “View Brochure” 链接,单击该链接,直接将用户引导到 PDF 文件。对于类别的图片,我们首先创建一个页面来从数据库检索和返回二进制数据,然后使用该页在 GridView 中显示每种类别的图片。

我们了解了如何显示二进制数据后 , 后续将介绍如何对含有二进制数据的数据库执行插入、更新和删除。在下一教程中,我们将了解怎样将上载的文件与其相应的数据库记录相关联。然后,我们将学习怎样更新现有的二进制数据,以及当与其关联的记录被删除后怎样删除二进制数据。

快乐编程!

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