在ASP.NET 2.0中操作数据之五十三:在Data Web控件显示二进制数据
导言:
在前面的教程我们阐述了应用程序处理二进制数据的2种模式,以及使用fileupload 控件从浏览器向服务器文件系统上传文件。当文件上传并存储在文件系统里时,应在相应的数据库记录里存储该文件的存储路径。
我们先来看如何为最终用户提供二进制数据。怎样展示二进制数据呢?这取决于其类型。比如图片,我们将其显示为image;如果是pdfs,microsoft word文档、zip文件或其它类型的数据,或许提供一个“download”链接比较妥当。
在本节,我们看如何在gridview和detailsview一类的数据web控件里呈现二进制数据,在后面的教程我们将注意力转向将上传文件和数据库联系起来。
第一步:提供brochurepath值
表categories的picture列存储相关类的图片信息。具体的讲,为16色的低质量位图,大小为172乘120像素,约11 kb。另外还包括一个约78字节的ole报头,在显示图片的时候需要将其剥离。为什么会有报头信息呢?因为数据库northwind源于微软的access数据库。在access里二进制数据ole类型来存储的,该类型会添加报头。现在,我们看如何从图片剥离报头,以便显示。在后面的教程我们将创建一个界面,将带报头的这些位图替换为不带报头的等价的jpg图片。
前面我们考察了如何使用fileupload控件,让我们继续为服务器文件系统添加文件。不过暂时不用更新categories表的brochurepath列,那是下一章的内容。我们现在需要手工为brochurepath赋值。
在本教程,当你下载东西时,可以看到在~/brochures7文件夹有7个pdf小册子,每个小册子对应一个种类,seafood除外。我故意没为seafood提供pdf小册子,以便探讨如何处理某些记录没有附带二进制数据的情况。在服务器资源管理器里右键点击categories,选“查看表数据”,输入文件路径,如图1所示。由于seafood类没有图片,将其brochurepath的值设为“null”。
图1:手工为表categories的brochurepath列键入值
第2步:在gridview里添加一个下载链接
当为表categories的brochurepath列赋值后,我们准备创建一个gridview用于展示每个种类,并附带一个链接下载每个类的小册子。在第4步我们将扩展gridview以显示每个类的图片。
打开binarydata文件夹的displayordownloaddata.aspx页面并进入设计模式,从工具箱里拖一个gridview控件到页面,设其id为categories,从其智能标签选择绑定到一个名为categoriesdatasource的objectdatasource控件。该控件调用类categoriesbll的getcategories()方法。
图2:创建一个名为categoriesdatasource的objectdatasource控件
图3:设置objectdatasource使用categoriesbll类
图4:调用getcategories()方法
完成设置后,visual studio自动的为categoryid, categoryname, description, numberofproducts和brochurepath生成boundfield。移除numberofproducts,因为getcategories()方法用不上,同样将categoryid移除了。分别把categoryname和 brochurepath的headertext属性改为“category”和“brochure”。做上述修改后,你的gridview and 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)。列出了所有的8个类,除了seafood,其它7个类的boundfield列里显示各自的brochurepath值。由于seafood的brochurepath为null值,看起来为空格。
图5:显示了每个类别的name, description和brochurepath值
与其显示brochurepath的text值,不如创建一个指向小册子的链接。移除brochurepath,代之以hyperlinkfield。设它的headertext属性为“brochure”,text属性为“view brochure”, datanavigateurlfields属性为“ brochurepath”。
图6:添加一个指向brochurepath的hyperlinkfield
这样将在gridview里添加一列链接,如图7所示。点“view brochure”时要么直接在浏览器显示pdf,要么提示用户下载该文件。这取决于浏览器的设置以及是否安装了pdf阅读器。
图7:点击“view brochure”访问某类的brochure
图8:显示某类的pdf文件
隐藏无小册子图片的类的“view brochure” 文本
如图7所示,不管某个类的brochurepath是否为null值,名为brochurepath的hyperlinkfield都呈现为其text属性(“view brochure”) 。当然,如果brochurepath为null值,链接只显示为文本(而不带下划线),就像seafood类一样(见图7)。与显示文本“view brochure”相比,更为可取的是将那些brochurepath值为空的类显示为“no brochure available”。
为达此目的,我们需要用到templatefield,使其产生一个基于brochurepath值的合适的结果。我们先来看看如何实现,就像在教程之12《在gridview控件中使用templatefield》一样。
在“编辑列”对话框里选中名为brochurepath的hyperlinkfield,再点“convert this field into a templatefield”链接,将其转换为templatefield。
图9:将hyperlinkfield转换为templatefield
这样将创建一个templatefield,其itemtemplate模板包含一个hyperlink web控件,该控件的navigateurl属性为brochurepath值。用下面的代码将其替换掉:
<asp:templatefield headertext="brochure"> <itemtemplate> <%# generatebrochurelink(eval("brochurepath")) %> </itemtemplate> </asp:templatefield>
然后,在asp.net页面的“后台代码”里添加一个protected类型的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值不为空时,将调用resolveurl(url)方法。该方法的作用在于将传入的相对路径转换为物理路径。比如应用程序的根目录在/tutorial55,resolveurl("~/brochures/meats.pdf")返回的路径是/tutorial55/brochures/meat.pdf.
图10为经过上述修改后的界面。我们注意到seafood类的brochurepath列现在显示为文本“no brochure available”.
图10:没有小册子的类将显示为文本“no brochure available”
第3步:新增页面以显示类的图片
当用户访问一个asp.net页面时,他将接收该页面的html代码。html代码仅仅包含了text文本,而并不包含任何的二进制数据。任何的二进制数据,比如图片,音乐文件、flash程序、windows media player视频等,以独立资源的形式存放于服务器。
html只包含了这些文件的引用,并不包含这些文件本身。
比如,在html里<img>元素用来引用一张图片,其src属性指向该图片文件,如:
<img src="mypicture.jpg" ... />
当浏览器收到html代码时,它向服务器发送获取图片的请求并将其显示在浏览器中,该模式对所有的二进制数据都适用。在第2步中,我们没有在页面的html标记里将小册子显示在浏览器,而是在html标记里提供一个超链接,当点击它是,导致浏览器直接请求pdf文件。
为了显示或允许用户下载储存在数据库中的二进制数据,我们需要另外创建一个页面,用于从数据库返回所需的数据。对我们的应用程序而言,由于直接存储在数据库中的二进制数据只有一项——类的图片,所以我们需要一个页面,当需要时从数据库返回某个特定类的图片。
在binarydata文件夹添加一个displaycategorypicture.aspx页面,注意不要使用母版页。该页面接受一个包含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)方法向客户端返回数据。不过在此之前先要剥离数据的ole报头。怎么实现呢?创建一个名为strippedimagedata的byte数组,它包含的字节刚好比picture列的数据少78。而array.copy方法将从category.picture的第78个字节开始复制数据(即刚好剥离ole报头)。
代码中的response.contenttype属性指定了要返回内容的mime type,以便浏览器知道如何显示数据。由于categories表的picture列存储的是位图图片,故在这里,位图图片的mime type是(image/bmp). 如果你忽视了mime type,绝大多数浏览器也可以正确的显示图像,因为,它们能根据图像文件的二进制数据的内容而推断其类型。即便如此,还是尽可能的使用mime type。
创建页面后,可以访问页面
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[]'”。原因有可能是如下2方面。第一,表categories的picture列允许为null值,而displaycategorypicture.aspx page页面总是假定传入的为非null值。当picture为null值时,不能直接访问categoriesdatatable的picture属性。如果你允许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的图片,当某个类没有图片时,就显示该图片。
另一种情况:当你在向导里选用“使用sql语句”的模式再次运行主查询时,它将影响getcategorywithbinarydatabycategoryid方法的select命令返回的列(换句话说,主查询没有返回picture列,再次运行主查询时将使getcategorywithbinarydatabycategoryid方法也不会返回picture列)。所以,应确保getcategorywithbinarydatabycategoryid方法的select命令返回picture列。
注意:每次访问displaycategorypicture.aspx页面时,都会访问数据库并返回所需的图片。如果图片自最近一次访问以来没有改变过的话,这样每次访问数据库再返回数据的做法效率是不高的。幸运的是,http允许使用conditional gets,这样的话,客户端使http请求发送一个if-modified-since http header。if-modified-since http header包含了客户端最近一次从服务器获取的数据以及时间。如果请求的内容没有发生改变,服务器响应为not modified status code (304),并不返回请求的内容。简而言之,如果请求的资源自最近一次访问以来没发送改变的话,服务器将不会回传该资源,以达到减轻服务器负荷的目的。
第四步:在gridview控件里显示category pictures
现在我们有一个web页面来显示某个特定种类的图片的。通过image web控件或 html <img>元素来指向displaycategorypicture.aspx?categoryid=categoryid页面,从而达到显示该图片的目的。我们可以在gridview控件或detailsview控件的 imagefield里显示图片。imagefield的dataimageurlfield属性、dataimageurlformatstring属性与hyperlinkfield的datanavigateurlfields属性、datanavigateurlformatstring属性用法相似。
让我们对displayordownloaddata.aspx页面里名为categories的gridview控件进行扩充。添加一个imagefield,设其dataimageurlfield属性为categoryid;
dataimageurlformatstring属性为displaycategorypicture.aspx?categoryid={0}。这样将为gridview增加一列,呈现为一个<img>元素,其src属性为displaycategorypicture.aspx?categoryid={0},其中{0}将由gridview row的categoryid值填充。
图12:为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小册子文件来说,我们提供了一个“view brochure”链接,当点击它时,直接将用户指向pdf小册子文件。对某个种类的图片,我们先是创建一个页面来从数据库获取并显示它,然后再在一个gridview控件里显示图片。既然看到了如何展示二进制数据,我们准备探讨如何对其展开插入、更新、删除操作。接下来的教程我们看如何将上传文件和相应的数据库记录联系起来。然后,再探讨如何更新现存的二进制数据,以及当删除数据库记录时如何删除相应的二进制数据。
祝编程快乐!
作者简介
本系列教程作者 scott mitchell,著有六本asp/asp.net方面的书,是4guysfromrolla.com的创始人,自1998年以来一直应用 微软web技术。大家可以点击查看全部教程《[翻译]scott mitchell 的asp.net 2.0数据教程》,希望对大家的学习asp.net有所帮助。