在ASP.NET 2.0中操作数据之五十五:编辑和删除现有的二进制数据

叶子的想法,你不用明白,而叶生两面,你必须明白。所以,有人在秋天看到的是生命的衰败和无奈,有人在秋天看到的是生命的辉煌和温怡。到门不敢提凡鸟,看竹何须问主人。

导言:

  在前面的3章里我们为处理二进制数据添加了很多的功能。我们首先在表Categories里添加BrochurePath列,并更新了体系结构。同样,为了处理表Categorie里现有的Picture列,我们在数据访问层和业务逻辑层里增加了相应的方法。同时我们创建一个页面,在GridView控件里显示二进制数据——包含一个指向说明小册子的下载链接,并将每个类的图片显示在<img>元素里。同时我们添加一个DetailsView控件,供用户添加新的类,并上传其图片和小册子数据。

  剩下的就是添加编辑和删除功能,本章我们将通过GridView控件内建的编辑和删除功能来实现。当编辑一个类时,我们允许用户用任意指定的图片将原来的换掉;也可以用新的小册子将现有的替换掉,甚至不再包含小册子文件。让我们开始吧!

第1步:更新数据访问层

  虽然数据访问层包含自动生成的Insert, Update和Delete方法,但它们都基于CategoriesTableAdapter的主查询,因此并不包含Picture列。自然,Insert和Update方法也不包含picture列的相应参数。就像56章做的那样,我们需要为更新Categories表而创建新的TableAdapter方法。

  右键点击CategoriesTableAdapter的顶部,选择“添加查询”,打开TableAdapter查询设置向导,我们首先选择“使用SQL语句”,点Next,再选“UPDATE”,再点Next.


图1:选择“UPDATE”选项

我们现在需要指定UPDATE SQL语句。向导自动创建一个基于TableAdapter主查询的UPDATE语句(它更新CategoryName, Description和BrochurePath值)。更新该语句以包含Picture列,以及@Picture参数,像如下这样:

UPDATE [Categories] SET
 [CategoryName] = @CategoryName,
 [Description] = @Description,
 [BrochurePath] = @BrochurePath ,
 [Picture] = @Picture
WHERE (([CategoryID] = @Original_CategoryID))

最后,向导要求我们为新的TableAdapter方法命名,我们取为UpdateWithPicture,再点Finish。


图2:为新方法命名为UpdateWithPicture

第2步:添加新的业务逻辑方法

除了更新DAL外,我们需要更新BLL以包含更新、删除类的方法。以下是表现层需要调用的方法:

为了删除一个类,我们使用CategoriesTableAdapter的自动生成的Delete方法,在类CategoriesBLL里添加如下的方法:

[System.ComponentModel.DataObjectMethodAttribute
 (System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteCategory(int categoryID)
{
 int rowsAffected = Adapter.Delete(categoryID);

 // Return true if precisely one row was deleted, otherwise false
 return rowsAffected == 1;
}

  本好代码教程,为了更新一个类,我们将创建2个方法。一个方法接受picture值,并调用我们刚刚添加到CategoriesTableAdapter里的UpdateWithPicture方法。另一个方法只接受CategoryName, Description和BrochurePath值, 并调用CategoriesTableAdapter类里自动生成的Update语句。为什么要使用2种方法呢?某些情况下,用户更新类时同时更新其图片,这时就需要上传一张新图片。上传图片的数据将在UPDATE语句里用到;另一种情况,用户只想更新类的name和description信息,因此我们需要使用2种更新方法。业务逻辑层会根据是否传入picture值来判断使用哪种方法。

  为达该目的,我们要在CategoriesBLL类里添加2个方法,名字都是UpdateCategory,第一个方法接受的参数包括3个string,1个byte数组和1个int;第二个方法接受的参数包括3个string和1个int。3个字符串参数代表类的name, description和brochure文件路径,byte数组包含的是类的picture数据,int代表类记录的CategoryID,我们注意到,当传入的byte数组为null时,第一个方法将调用第二个方法。

[System.ComponentModel.DataObjectMethodAttribute
 (System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateCategory(string categoryName, string description,
 string brochurePath, byte[] picture, int categoryID)
{
 // If no picture is specified, use other overload
 if (picture == null)
 return UpdateCategory(categoryName, description, brochurePath, categoryID);

 // Update picture, as well
 int rowsAffected = Adapter.UpdateWithPicture
 (categoryName, description, brochurePath, picture, categoryID);

 // Return true if precisely one row was updated, otherwise false
 return rowsAffected == 1;
}

[System.ComponentModel.DataObjectMethodAttribute
 (System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateCategory(string categoryName, string description,
 string brochurePath, int categoryID)
{
 int rowsAffected = Adapter.Update
 (categoryName, description, brochurePath, categoryID);

 // Return true if precisely one row was updated, otherwise false
 return rowsAffected == 1;
}

第3步:拷贝功能

  在上一章里,我们创建了一个UploadInDetailsView.aspx页面,在一个GridView控件列出所有的类,再通过一个DetailsView控件来添加新的类。在本好代码教程,我们将扩展GridView控件以支持编辑和删除功能。不过我们不再使用UploadInDetailsView.aspx页面,让我们在~/BinaryData文件夹里创建一个新页面,UpdatingAndDeleting.aspx,将UploadInDetailsView.aspx页面的声明代码复制并粘贴到页面UpdatingAndDeleting.aspx.

  打开UploadInDetailsView.aspx页面,将其<asp:Content>元素里的声明代码复制下来,就像图3那样。接下来,打开UpdatingAndDeleting.aspx页面,把代码粘贴在<asp:Content>元素里。同样的,将UploadInDetailsView.aspx页面的后台代码拷贝到UpdatingAndDeleting.aspx。


图3:将UploadInDetailsView.aspx页面的声明代码拷贝下来

完成后,登录UpdatingAndDeleting.aspx页面,你将会看到相同的输出效果。感觉用起来和UploadInDetailsView.aspx页面一样。

第4步:添加ObjectDataSource和GridView的删除功能

  就像在好代码教程16《概述插入、更新和删除数据》里探讨的一样,只要GridView控件绑定的数据源支持“删除”功能,我们就可以为GridView控件启用删除功能。不过,GridView控件绑定的ObjectDataSource(也就是CategoriesDataSource)目前并不支持删除。

  为支持删除,在ObjectDataSource的智能标签里点“配置数据源”,一直点到“定义数据方法”界面。虽然当前只指定了ObjectDataSource控件InsertMethod属性和SelectMethod属性,但向导自动地分别为UPDATE标签和DELETE标签指定UpdateCategory方法和DeleteCategory方法。为什么呢?因为我们在CategoriesBLL类里为上述2种方法使用了DataObjectMethodAttribute属性,作用是分别使其成为默认的“更新”和“删除”方法。

  不过现在我们在UPDATE标签的下拉列表里选“(None)”, 而 DELETE标签里仍然为DeleteCategory方法。我们将在第6步添加更新功能。


图4:设置ObjectDataSource控件使用DeleteCategory方法

  注意:完成设置后,Visual Studio会问你是否“刷新列和主键”,选择No,因为选择Yes将会把我们自己定制的任何列覆盖掉。

  现在,ObjectDataSource控件将包含DeleteMethod属性和对应的DeleteParameter参数。我们记得在以前的好代码教程提到过,当使用向导指定方法时,Visual Studio会自动的将ObjectDataSource控件的OldValuesParameterFormatString属性设置为original_{0},这将导致更新和删除时出现问题。为此,要么将清除该属性,要么将其设置为默认的{0}值。对该属性的更详细讨论见好代码教程16《概述插入、更新和删除数据》

完成后,ObjectDataSource控件的声明代码看起来应该像下面的一样:

<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
 OldValuesParameterFormatString="{0}" SelectMethod="GetCategories"
 TypeName="CategoriesBLL" InsertMethod="InsertWithPicture"
 DeleteMethod="DeleteCategory">
 <InsertParameters>
 <asp:Parameter Name="categoryName" Type="String" />
 <asp:Parameter Name="description" Type="String" />
 <asp:Parameter Name="brochurePath" Type="String" />
 <asp:Parameter Name="picture" Type="Object" />
 </InsertParameters>
 <DeleteParameters>
 <asp:Parameter Name="categoryID" Type="Int32" />
 </DeleteParameters>
</asp:ObjectDataSource>

设置完ObjectDataSource后,就可以启用GridView的删除功能了,方法是点击其智能标签里的“删除选项”。这将使GridView增加一个CommandField,其ShowDeleteButton属性为true。


图5:启用GridView控件的删除功能

  花几分钟测试删除功能。由于表Products和表Categories之间有一个外键CategoryID,当你删除现有的8个类中的任何一个时,你会得到一个外键约束冲突异常。为顺利的实现测试,我们需要添加一个附带图片和说明小册子的新类,如图6所示,小册子为Test.pdf,图7为添加了测试类的GridView控件界面。


图6:添加一个附带Brochure和Image文件的测试类


图7:添加测试类后,显示在GridView控件里

在Visual Studio里刷新解决资源管理器,你会在文件夹~/Brochures里看到Test.pdf文件(见图8)

下一步,点击Test类的Delete链接,页面回传,引发CategoriesBLL的DeleteCategory 方法,该方法又调用DAL层的Delete方法,向数据库发送适当的DELETE命令。最后数据重新绑定到GridView控件,Test类将不再显示出来。

虽然已经成功地将Test类从Categories表删除,但存储在文件系统的对应小册子仍旧存在,刷新解决资源管理器,你将发现Test.pdf依然放在~/Brochures文件夹里。


图8:Test.pdf文件并没有从文件系统删除

第5步:删除残存的Brochure文件

  未将二进制数据存储进数据库时面临的一个问题便是:当删除一条数据库记录时,我们需要另外采取步骤来删除该记录对应的二进制数据文件。当执行delete命令时,会发生一些事前事件和事后事件(pre- and post-action events),我们需要创建对应的事件处理器。在Categories表的记录被删除之前,我们需要确定对应PDF文件的路径,但在删除记录之前我们不会删除其对应的PDF文件,以防发生异常或记录最终未被删除的情况。

  从事件发生的时间先后顺序来看,GridView控件的RowDeleting事件在调用ObjectDataSource控件的delete命令前发生;而RowDeleted事件在调用ObjectDataSource控件的delete命令之后再发生。创建这2个事件处理器,代码如下:

// A page variable to "remember" the deleted category's BrochurePath value
string deletedCategorysPdfPath = null;

protected void Categories_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
 // Determine the PDF path for the category being deleted...
 int categoryID = Convert.ToInt32(e.Keys["CategoryID"]);

 CategoriesBLL categoryAPI = new CategoriesBLL();
 Northwind.CategoriesDataTable categories =
 categoryAPI.GetCategoryByCategoryID(categoryID);
 Northwind.CategoriesRow category = categories[0];

 if (category.IsBrochurePathNull())
 deletedCategorysPdfPath = null;
 else
 deletedCategorysPdfPath = category.BrochurePath;
}

protected void Categories_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
 // Delete the brochure file if there were no problems deleting the record
 if (e.Exception == null)
 {
 // Is there a file to delete?
 if (deletedCategorysPdfPath != null)
 {
  System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath));
 }
 }
}

  在RowDeleting事件处理器里,从GridView控件的DataKeys集合里获取被删记录的CategoryID值,而在这里,我们通过e.Keys来访问DataKeys集合。接着,调用类CategoriesBLL的GetCategoryByCategoryID(categoryID)方法来返回被删记录的信息,若返回的BrochurePath值不为NULL,那么将其赋值给页面参数deletedCategorysPdfPath,再在RowDeleted事件处理器里删除文件。

  注意:在RowDeleting事件处理器里,我们没有返回被删记录的BrochurePath信息,而是将BrochurePath添加到GridView的DataKeyNames属性,再通过访问e.Keys来获取该记录的值。这样做虽然稍微增大了GridView的视图状态,但减少了必要的代码,也省了一步访问数据库。

  调用ObjectDataSource控件的delete命令后,紧接着发生GridView控件的RowDeleted事件,如果删除过程没有异常且deletedCategorysPdfPath值不为空,那就将对应的PDF文件从文件系统删除。我们注意到,代码没有删除类的picture,那是因为picture数据是直接存储在数据库里的,当删除记录时就一起删除了。

  添加完上述2个事件处理器后,再次测试删除。当删除某个类时,其对应的PDF文件也删除了。

  下面我们深入研究添加更新功能以应对类的brochure和picture.第6步探讨更新brochure信息的技术,第7章探讨更新picture。

第6步:更新类的Brochure

  就像在好代码教程16《概述插入、更新和删除数据》里探讨的一样,如果GridView的数据源控件支持编辑,那么我们就可以启用GridView控件的编辑功能。当前,名为CategoriesDataSource的ObjectDataSource控件并不支持编辑,那让我们添加吧。

  点击ObjectDataSource控件的“设置数据源”链接,一直点到“定义数据方法”界面。由于在CategoriesBLL里对重载的UpdateCategory方法使用了DataObjectMethodAttribute属性,UPDATE标签的下拉列表自动的选择了该方法,它包含4个输入参数(不包含Picture)。我们选择另一个包含5个输入参数的重载的UpdateCategory方法。


图9:设置ObjectDataSource控件使用包含Picture参数的UpdateCategory方法

  ObjectDataSource控件现在包含了UpdateMethod属性以及相应的UpdateParameters参数集。就像在第4步提到的一样,当使用设置向导时,Visual Studio会将ObjectDataSource控件的OldValuesParameterFormatString属性设置为original_{0},这导致调用update和delete方法时出现问题。因此,要么将该属性清除,要么设该属性为{0}。

完成后,ObjectDataSource控件的声明代码看起来应该和下面的差不多:

<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
 OldValuesParameterFormatString="{0}" SelectMethod="GetCategories"
 TypeName="CategoriesBLL" InsertMethod="InsertWithPicture"
 DeleteMethod="DeleteCategory" UpdateMethod="UpdateCategory">
 <InsertParameters>
 <asp:Parameter Name="categoryName" Type="String" />
 <asp:Parameter Name="description" Type="String" />
 <asp:Parameter Name="brochurePath" Type="String" />
 <asp:Parameter Name="picture" Type="Object" />
 </InsertParameters>
 <DeleteParameters>
 <asp:Parameter Name="categoryID" Type="Int32" />
 </DeleteParameters>
 <UpdateParameters>
 <asp:Parameter Name="categoryName" Type="String" />
 <asp:Parameter Name="description" Type="String" />
 <asp:Parameter Name="brochurePath" Type="String" />
 <asp:Parameter Name="picture" Type="Object" />
 <asp:Parameter Name="categoryID" Type="Int32" />
 </UpdateParameters>
</asp:ObjectDataSource>

要启用编辑功能,从GridView控件的智能标签里选“编辑”。这将设置CommandField的ShowEditButton属性为true,结果是为每行添加一个Edit按钮(当记录处于编辑状态时,将呈现为Update和Cancel按钮)


图10:启用GridView控件的编辑功能

  从浏览器查看该页面,点某条记录的Edit按钮。CategoryName和Description列呈现为一个文本框。由于BrochurePath TemplateField没有EditItemTemplate模板,所以它依旧呈现其ItemTemplate模板——一个指向brochure的链接。Picture列呈现为一个文本框,并且该Picture ImageField的Text属性被指派为DataImageUrlField值,在这里,即CategoryID.


图11:BrochurePath列没有编辑界面

定制BrochurePath编辑界面

我们可以为BrochurePath TemplateField创建一个编辑界面,我们可以选择:

.维持原样
.上传新的brochure以作更新
.将brochure删除(这样一来,类就没有对应的brochure了)

我们也应该更新Picture ImageField的编辑界面,不过我们将放在第7步来讨论。

  在GridView控件的智能标签里选择“编辑模板”,再从下拉列表里选BrochurePath TemplateField的EditItemTemplate模板。在模板里添加一个RadioButtonList Web控件,其ID为BrochureOptions;AutoPostBack属性为true.再在属性窗口里点Items属性的椭圆型区域,进入ListItem Collection Editor界面,分别添加值为1,2,3的选项:

.Use current brochure
.Remove current brochure
.Upload new brochure

设第一个ListItem的Selected属性为true.


图12:为RadioButtonList控件添加3个ListItems

在RadioButtonList控件下面,添加一个FileUpload控件,ID为BrochureUpload,设其Visible属性为false。


图13:在EditItemTemplate模板里添加RadioButtonList和FileUpload控件

RadioButtonList控件为用户提供了3个选择,只有当选择“Upload new brochure”时, FileUpload控件才会展现出来。为此,我们为RadioButtonList控件的SelectedIndexChanged事件创建事件处理器,如下:

protected void BrochureOptions_SelectedIndexChanged(object sender, EventArgs e)
{
 // Get a reference to the RadioButtonList and its Parent
 RadioButtonList BrochureOptions = (RadioButtonList)sender;
 Control parent = BrochureOptions.Parent;

 // Now use FindControl("controlID") to get a reference of the
 // FileUpload control
 FileUpload BrochureUpload =
 (FileUpload)parent.FindControl("BrochureUpload");

 // Only show BrochureUpload if SelectedValue = "3"
 BrochureUpload.Visible = (BrochureOptions.SelectedValue == "3");
}

  由于RadioButtonList控件和FileUpload控件同时出现在一个模板里,我们需要通过编程来访问这2个控件。在SelectedIndexChanged事件处理器里,我们通过输入参数sender来引用RadioButtonList控件。为了获取FileUpload控件,我们需要使用RadioButtonList的父控件(parent control),并使用FindControl("controlID")方法。一旦我们同时获取了RadioButtonList和FileUpload控件时,只要RadioButtonList控件的SelectedValue值等于3,即“Upload new brochure” ListItem的值时,将FileUpload控件的Visible属性设置为true 。

  添加完上述代码后,花几分钟时间来测试编辑页面。点击某行的Edit按钮,默认是选中“Use current brochure”项,改选另一项,页面产生回传,如果是选择第3项,则FileUpload控件将会显示出来,否则处于隐身状态。图14显示点击Edit按钮的情形,而图15则是选择“Upload new brochure”时的情形。


图14:默认选择“Use current brochure”项


图15:选择“Upload new brochure”时FileUpload控件显示出来

保存Brochure文件并更新BrochurePath列

当点击GridView控件的Update按钮时,触发RowUpdating事件,调用ObjectDataSource控件的update命令,然后触发GridView控件的RowUpdated事件。跟deleting流程类似,我们需要创建这些事件的处理器。在RowUpdating事件处理器里,我们需要根据RadioButtonList的SelectedValue值来判断下一步怎么做。

.如果SelectedValue值为1,我们将保持rochurePath不变。所以我们将ObjectDataSource控件的brochurePath参数设置为当前处于编辑状态记录的BrochurePath值,方法为e.NewValues["brochurePath"] = value.

.如果SelectedValue值为2,意味着将BrochurePath设为NULL。为此,我们需要将ObjectDataSource控件的brochurePath参数设为Nothing,结果就是在UPDATE命令里使用NULL。如果存在对应的brochure文件,我们必须将其删除,前提是没有抛出任何的异常。

.如果SelectedValue值为3,我们必须确保用户已经上传了一个PDF文件并将其保存在文件系统,然后更新记录的BrochurePath值。我们要先将被替换的前一个文件删除掉,当然前提是没有引发异常。

在上一章里,当在DetailsView控件里添加新记录时,触发DetailsView控件的ItemInserting事件。在本章,当RadioButtonList控件的SelectedValue为3时(即我们选择Upload new brochure时),接下来要采取的步骤实际上与DetailsView控件的ItemInserting事件处理器实现的功能相似。根据实现的功能,我划分为2个方法:

.ProcessBrochureUpload(FileUpload, out bool):它以一个FileUpload控件实例为输入参数,结果为一个布尔值(Boolean)。根据该布尔值判断是否继续更新或删除操作,抑或取消操作。如果存在上传文件该方法就返回其路径,反之返回null。

.DeleteRememberedBrochurePath:如果页面变量deletedCategorysPdfPath不为null,则删除该参数指定的文件。

下面是上述2种方法的代码。注意ProcessBrochureUpload方法和DetailsView控件的ItemInserting事件处理器有某些相似性,在本章,我们更新DetailsView控件的事件处理器以使用这些新方法。下载本章的代码,查看我们对DetailsView控件的事件处理器所做的修改。

private string ProcessBrochureUpload
 (FileUpload BrochureUpload, out bool CancelOperation)
{
 CancelOperation = false; // by default, do not cancel operation

 if (BrochureUpload.HasFile)
 {
 // Make sure that a PDF has been uploaded
 if (string.Compare(System.IO.Path.GetExtension(BrochureUpload.FileName),
  ".pdf", true) != 0)
 {
  UploadWarning.Text =
  "Only PDF documents may be used for a category's brochure.";
  UploadWarning.Visible = true;
  CancelOperation = true;
  return null;
 }

 const string BrochureDirectory = "~/Brochures/";
 string brochurePath = BrochureDirectory + BrochureUpload.FileName;
 string fileNameWithoutExtension =
  System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName);

 int iteration = 1;

 while (System.IO.File.Exists(Server.MapPath(brochurePath)))
 {
  brochurePath = string.Concat(BrochureDirectory, fileNameWithoutExtension,
  "-", iteration, ".pdf");
  iteration++;
 }

 // Save the file to disk and set the value of the brochurePath parameter
 BrochureUpload.SaveAs(Server.MapPath(brochurePath));
 return brochurePath;
 }
 else
 {
 // No file uploaded
 return null;
 }
}

private void DeleteRememberedBrochurePath()
{
 // Is there a file to delete?
 if (deletedCategorysPdfPath != null)
 {
 System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath));
 }
}

在GridView控件的RowUpdating和RowUpdated事件处理器里使用上面2个方法,如下:

protected void Categories_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
 // Reference the RadioButtonList
 RadioButtonList BrochureOptions =
 (RadioButtonList)Categories.Rows[e.RowIndex].FindControl("BrochureOptions");

 // Get BrochurePath information about the record being updated
 int categoryID = Convert.ToInt32(e.Keys["CategoryID"]);

 CategoriesBLL categoryAPI = new CategoriesBLL();
 Northwind.CategoriesDataTable categories =
 categoryAPI.GetCategoryByCategoryID(categoryID);
 Northwind.CategoriesRow category = categories[0];

 if (BrochureOptions.SelectedValue == "1")
 {
 // Use current value for BrochurePath
 if (category.IsBrochurePathNull())
  e.NewValues["brochurePath"] = null;
 else
  e.NewValues["brochurePath"] = category.BrochurePath;
 }
 else if (BrochureOptions.SelectedValue == "2")
 {
 // Remove the current brochure (set it to NULL in the database)
 e.NewValues["brochurePath"] = null;
 }
 else if (BrochureOptions.SelectedValue == "3")
 {
 // Reference the BrochurePath FileUpload control
 FileUpload BrochureUpload =
  (FileUpload)Categories.Rows[e.RowIndex].FindControl("BrochureUpload");

 // Process the BrochureUpload
 bool cancelOperation = false;
 e.NewValues["brochurePath"] =
  ProcessBrochureUpload(BrochureUpload, out cancelOperation);

 e.Cancel = cancelOperation;
 }
 else
 {
 // Unknown value!
 throw new ApplicationException(
  string.Format("Invalid BrochureOptions value, {0}",
  BrochureOptions.SelectedValue));
 }

 if (BrochureOptions.SelectedValue == "2" ||
 BrochureOptions.SelectedValue == "3")
 {
 // "Remember" that we need to delete the old PDF file
 if (category.IsBrochurePathNull())
  deletedCategorysPdfPath = null;
 else
  deletedCategorysPdfPath = category.BrochurePath;
 }
}

protected void Categories_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
 // If there were no problems and we updated the PDF file,
 // then delete the existing one
 if (e.Exception == null)
 {
 DeleteRememberedBrochurePath();
 }
}

注意:RowUpdating事件处理器是如何根据SelectedValue值的不同而使用一系列的条件语句来实现相应的功能。

使用上面的代码,我们就可以编辑一个类了,使用其当前的brochure,或不使用brochure,再或者使用一个新的brochure。在RowUpdating和RowUpdated事件处理器里设置断点(breakpoints)吧,以便更好的理解处理流程。

第7步:上传新图片

  Picture ImageField的编辑界面呈现为一个文本框,里面显示的是DataImageUrlField 属性的值。在编辑流程,GridView控件向ObjectDataSource传入一个参数,参数名为ImageField的DataImageUrlField属性;参数值为在编辑界面输入文本框里的值。当图片是存储在文件系统,且DataImageUrlField属性包含的是访问该图片的完整URL时,这样做是恰当的。在这种情况下,在编辑界面里,文本框将会显示图片的URL。毫无疑问,默认的界面不允许用户上传新的图片,但用户却可以修改图片的URL值。不过,在本好代码教程不会出现这种情况,因为Picture数据是直接存储在数据库的,且DataImageUrlField属性被设为CategoryID值。

  为了更好的理解在本好代码教程里编辑某行的ImageField时将会发生上什么,我们做如下假设:用户编辑一个CategoryID值为10行,Picture ImageField呈现为一个文本框,显示10,假设用户将其改为50后点Update按钮,页面回传,GridView控件最初产生一个名为CategoryID,值为50的参数。在GridView传递此参数(连同参数CategoryName和参数Description一起)以前,对DataKeys集添加值。因此,将当前行的CategoryID值10,覆盖掉。简言之,ImageField的编辑界面没有对本章好代码教程的编辑流程产生任何影响,因为ImageField的DataImageUrlField属性和DataKey值都是同一个值。

  当图片存储在数据库时,ImageField将其显示出来也很容易。不过在编辑界面里我们不需要使用文本框,而提供一个FileUpload控件供最终用户更改图片时使用。与BrochurePath不同,我们不允许类的图片为空——用户要么提供新图片要么使用当前的图片。

  为定制ImageField的编辑界面,我们需要将其转化为一个TemplateField。在GridView控件的智能标签里点击“编辑列”,进入后选中ImageField,再点击“Convert this field into a TemplateField”链接。


图16:将ImageField转换为TemplateField

  转换后的TemplateField由2个模版构成。就像下面的声明代码显示的那样,ItemTemplate模版包含一个Image Web控件,其ImageUrl属性由一个数据绑定语法指定,该数据绑定语法基于ImageField的DataImageUrlField和 DataImageUrlFormatString属性。而EditItemTemplate模版则包含一个TextBox,其Text属性绑定到DataImageUrlField属性的值。

<asp:TemplateField>
 <EditItemTemplate>
 <asp:TextBox ID="TextBox1" runat="server"
  Text='<%# Eval("CategoryID") %>'></asp:TextBox>
 </EditItemTemplate>
 <ItemTemplate>
 <asp:Image ID="Image1" runat="server"
  ImageUrl='<%# Eval("CategoryID",
  "DisplayCategoryPicture.aspx?CategoryID={0}") %>' />
 </ItemTemplate>
</asp:TemplateField>

  我们需要更新EditItemTemplate模版以包含一个FileUpload控件。从GridView控件的智能标签点“编辑模版”,再在下拉列表选择Picture TemplateField的EditItemTemplate模版。在模版里你会看见一个TextBox,将其删除。从工具箱里拖一个FileUpload控件到页面,设其ID为PictureUpload。同时在模版里添加如下的文本:“To change the category's picture, specify a new picture. To keep the category's picture the same, leave the field empty”。


图17:在EditItemTemplate模版里添加一个FileUpload控件


完成定制该编辑界面后,在浏览器里查看。在只读模式里,类的图片和以前没什么两样,当点击Edit按钮时,picture列将呈现一段文本和一个FileUpload控件。


图18:编辑界面包含一个FileUpload控件

  记得我们设置ObjectDataSource控件调用CategoriesBLL的UpdateCategory方法,该方法的一个输入参数为数组,用于处理图片的数据。如果该数组为null值,则调用另一个重载的UpdateCategory方法,该重载的UpdateCategory方法的UPDATE SQL语句不会更改Picture列,因此类的图片不会由任何变化。在GridView控件的RowUpdating事件处理器里,我们编程访问名为PictureUpload的FileUpload控件,判断是否上传了文件。如果没有文件上传,我们将不会为参数picture指定值;反之,如果上传了文件,我们将确保其为JPG格式的文件,并通过参数picture将其传给ObjectDataSource控件。

  就像第6步里的代码一样,我们此时将要用到的绝大多数的代码已经存在于DetailsView控件的ItemInserting事件处理器里了。现在我们创建一个新的方法ValidPictureUpload,并更新ItemInserting事件处理器以使用该方法。

  在GridView控件的RowUpdating事件处理器的开头部分添加如下的代码,这很重要,因为我们不希望将一个不符合条件的上传文件存储在文件系统。

// Reference the PictureUpload FileUpload
FileUpload PictureUpload =
 (FileUpload)Categories.Rows[e.RowIndex].FindControl("PictureUpload");
if (PictureUpload.HasFile)
{
 // Make sure the picture upload is valid
 if (ValidPictureUpload(PictureUpload))
 {
 e.NewValues["picture"] = PictureUpload.FileBytes;
 }
 else
 {
 // Invalid file upload, cancel update and exit event handler
 e.Cancel = true;
 return;
 }
}

  ValidPictureUpload(FileUpload)方法只有一个FileUpload控件类型的输入参数,通过检查上传文件的扩展符以确保上传的文件为JPG格式。只有当上传了文件时才会调用该方法;如果没有文件上传,参数picture就只能使用其默认值—null。如果上传了图片,且ValidPictureUpload方法返回值true,将用图片的二进制数据对参数picture赋值。如果ValidPictureUpload方法返回值false,则取消更新,并退出事件处理器。

ValidPictureUpload(FileUpload)方法的代码如下:

private bool ValidPictureUpload(FileUpload PictureUpload)
{
 // Make sure that a JPG has been uploaded
 if (string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName),
  ".jpg", true) != 0 &&
 string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName),
  ".jpeg", true) != 0)
 {
 UploadWarning.Text =
  "Only JPG documents may be used for a category's picture.";
 UploadWarning.Visible = true;
 return false;
 }
 else
 {
 return true;
 }
}

第8步:将原始几个类的图片替换为JPG格式

回想起最开始的那8个类的图片为位图文件其包含一个OLE报头。现在我们添加了新功能以编辑现有记录的图片,花几分钟将这些位图文件替换为JPG文件。如果你想使当前类的图片不变,你可以通过下面的布置将其转换为JPG格式:

1.将这些位图保存在硬盘。在浏览器里访问UpdatingAndDeleting.aspx页面,对这8个类的图片,点右键,选则保存图片。

2.在一个图片编辑器(比如Microsoft Paint)软件里打开图片。

3.将图片保存为JPG格式

4.在编辑界面里,用JPG图片更新类的picture

完成更新并上传JPG图片之后,图片不会呈现在浏览器里,原因是DisplayCategoryPicture.aspx将尝试对最开始8个类的图片剥离OLE报头。怎样修正呢?我们将剥离OLE报头的代码移除。这样,DisplayCategoryPicture.aspx页面的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];

 // For new categories, images are JPGs...
 
 // Output HTTP headers providing information about the binary data
 Response.ContentType = "image/jpeg";

 // Output the binary data
 Response.BinaryWrite(category.Picture);
}

注意:UpdatingAndDeleting.aspx页面的编辑和添加界面要稍微复杂一点。DetailsView和GridView控件里的CategoryName和Description BoundFields应当转换成TemplateFields;另外由于CategoryName不能为NULL值,应对其添加一个RequiredFieldValidator控件。此外,Description应修改为允许换行的的文本框(multi-line TextBox),我将这些留给读者作为练习。

总结:
  本篇为处理二进制数据的完结篇,在本章以及前3章我们考察了如何将二进制数据存放在文件系统或直接存储在数据库里。用户在硬盘里选择一个文件并将其上传到服务器,再存放在文件系统或数据库。ASP.NET 2.0的FileUpload控件提供了上传的界面。然而,就像在好代码教程《使用FileUpload上传文件》里提到的那样,FileUpload控件控件只适合于上传小于1MB的文件。我们也探讨了如何编辑和删除当前记录的二进制数据。

在接下来的一系列好代码教程里,我们探讨各种缓存技术。使用缓存可以提升应用程序的整体性能。

  祝编程快乐!

作者简介

  本系列好代码教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部好代码教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据好代码教程》,希望对大家的学习ASP.NET有所帮助。

本文在ASP.NET 2.0中操作数据之五十五:编辑和删除现有的二进制数据到此结束。荆棘的存在是为了野草不轻易地任人践踏。小编再次感谢大家对我们的支持!

您可能有感兴趣的文章
详解免费开源的DotNet任务调度组件Quartz.NET(.NET组件介绍之五)

《解剖PetShop》之五:PetShop之业务逻辑层设计

在ASP.NET 2.0中操作数据之五十:为GridView控件添加Checkbox

在ASP.NET 2.0中操作数据之五十一:从GridView的页脚插入新记录

在ASP.NET 2.0中操作数据之五十四:添加新记录时包含一个文件上传选项