更新与删除现有二进制数据56

简介

在前面的三篇教程中 , 我们添加了许多功能来处理二进制数据。开始时我们在 Categories 表中添加了一个 BrochurePath 列并相应地更新了架构。我们还添加了一些数据访问层和业务逻辑层方法以处理 Categories 表的现有 Picture 列,该列含有图像文件的二进制内容。我们建立了 web 页面以在 GridView 中显示二进制数据 – 小册子的一个下载链接,分类的图片显示于 <img> 元素中。我们还增加了 DetailsView 以使用户能够添加新分类并上传新分类的小册子和图片数据。

剩余要实现的是对分类进行编辑和删除的功能,我们将在此教程中利用 GridView 的内置的编辑和删除特性来实现这些功能。在编辑分类时,用户将可以选择上传新图片,也可以选择让该分类继续使用现有的图片。对于小册子,用户有三种选择:使用现有的小册子、上传新的小册子、或指示该分类不再有相关的小册子。让我们开始吧 !

步骤1 : 更新数据访问层

DAL 有自动生成的插入、更新和删除方法 , 但这些方法是基于 CategoriesTableAdapter 的主查询而生成的 , 而该主查询并不包括 Picture 列。因此,这些插入、更新方法没有相应参数来指定分类的图片的二进制数据。如同 上一篇教程 ,在本教程中我们也需要创建一个新的 TableAdapter 方法以便在指定二进制数据时可以更新 Categories 表。

打开强类型DataSet , 在设计器上右键单击 CategoriesTableAdapter 的标题 , 然后从关联菜单中选择Add Query 以启动 TableAdapter Query Configuration Wizard 。该向导首先向我们询问该 TableAdapter 查询用以访问数据库的方式。选择 “Use SQL statements” 后单击 Next 。下一步提示我们选择要生成的查询的类型。因为当前创建查询是为了向 Categories 表添加一个新记录,所以在这里选择 “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 : 将新的 TableAdapter 方法命名为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; 
}

本教程中 , 我们将创建两个更新分类的方法 — 一个方法接受二进制图形数据并调用我们刚才添加到CategoriesTableAdapter 中的UpdateWithPicture 方法 , 另一个方法只接受 CategoryName 、Description 和 BrochurePath 值并使用CategoriesTableAdapter 类的自动生成的 Update 语句。使用两个方法的原因是,在一些情况下,用户可能想要在更新分类的其它字段的同时更新图片,此种情况下,用户必须上传新的图片。因而,在 UPDATE 语句中可以使用上传图片的二进制数据。在另一些情况下,用户可能只有意于更新其它的字段,比方说,名称和说明字段。但如果 UPDATE 期待的仍然是针对 Picture 列的二进制数据,我们同样就必须提供这一信息。这可能会需要一次针对数据库的额外的数据往返,以便取回要编辑的记录的图片数据。因此,我们需要两个 UPDATE 方法。业务逻辑层将会根据在更新分类时是否提供了图片数据来决定使用哪个方法。

为此 , 向 CategoriesBLL 类中添加两个方法 , 两个方法具有同一名称 :UpdateCategory 。第一个方法应接受三个字符串、一个字节数组及一个整型数作为其输入参数;第二个方法只应接受三个字符串和一个整型数。三个字符串分别用于分类的名称、说明及小册子文件路径,字节数组用于分类的图片的二进制内容,整型数用于指示要更新的记录的 CategoryID 。请注意,如果传入的字节数组为空,则第一个重载方法会调用第二个:

[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 页面的 code-behind 类中的代码复制到 UpdatingAndDeleting.aspx 中。

图3 : 复制 UploadInDetailsView.aspx 中的声明式标记

在复制完声明式标记和代码后 , 访问UpdatingAndDeleting.aspx , 此时应能看到与上一篇教程中的UploadInDetailsView.aspx 页面相同的输出并且应具有与该页面相同的用户体验。

步骤4 : 在ObjectDataSource 和GridView 中添加删除支持

在以前的 数据插入、更新和删除概述 教程中我们讲过 ,GridView 提供内置的删除功能 , 如果grid 的底层数据源支持删除 , 则通过选中一个复选框就可以启用这些功能。目前, GridView 绑定到的 ObjectDataSource (CategoriesDataSource) 不支持删除。

下面我们来弥补这一点。在 ObjectDataSource 的智能标记中,单击 Configure Data Source 选项以启动相应向导。第一个屏幕显示 ObjectDataSource 已配置为和 CategoriesBLL 类一起使用。单击 Next 。目前只指定了 ObjectDataSource 的 InsertMethod 和 SelectMethod 属性。但向导自动在 UPDATE 和 DELETE 选项卡的下拉列表中分别填充了 UpdateCategory 和 DeleteCategory 方法。这是因为,在 CategoriesBLL 类中我们已用 DataObjectMethodAttribute 将这些方法标记为默认的更新和删除方法。

现在 , 将UPDATE 选项卡的下拉列表设置为 “(None)” , 但保留DELETE 选项卡的下拉列表设置不变 , 仍为 DeleteCategory 。我们将在步骤 6 中返回此向导来添加更新支持。

图4 : 配置 ObjectDataSource , 使其使用 DeleteCategory 方法

注意 : 在结束向导时 ,Visual Studio 可能会询问是否要 “Refresh Fields and Keys” , 该刷新会重新生成 Web 数据控件字段。选择 No ,因为选择 Yes 会将我们刚才定制的任何字段覆盖掉。

现在 ,ObjectDataSource 包含一个 DeleteMethod 属性值以及一个 DeleteParameter 参数。我们记得在使用该向导指定方法时 ,Visual Studio 将 ObjectDataSource 的OldValuesParameterFormatString 属性设置为 original_{0} , 而这在进行更新与删除方法的调用时会引发问题。因此,或者彻底清除该属性,或者将其重新设置为默认值 {0}。如果需要对此 ObjectDataSource 属性刷新内存,参见 数据插入、更新和删除概述 教程。

在结束该向导并修改 OldValuesParameterFormatString 之后, 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 的智能标记中的 “Enable Deleting” 复选框以将删除功能添加到GridView 中。此操作将一个 CommandField 添加到 GridView 中,其 ShowDeleteButton 属性设置为 True 。

图5 : 启用 GridView 对删除的支持

我们花些时间来测试一下该删除功能。 由于 Products 表的 CategoryID 和 Categories 表的 CategoryID 之间有一个外键,当你删除现有的前八个分类中的任何一个时,你会得到一个外键约束违例异常。 为了测试该功能,添加一个新的分类,提供一个小册子和图片。我的一个测试分类,如图 6 所示,包含一个名为 Test.pdf 的测试小册子文件和一个测试图片。图 7 为添加了该测试分类后的 GridView 视图。

图6 : 添加一个带有小册子和图片的测试分类

图7 : 插入测试分类后 , 该分类显示于GridView 中

在Visual Studio 中 , 刷新 Solution Explorer 。现在,在 ~/Brochures 文件夹下应能看到一个新的文件, Test.pdf (参见图 8 )。

然后 , 单击该测试分类一行中的 Delete 链接 , 该操作会使页面回传且触发 CategoriesBLL 类的 DeleteCategory 方法。该方法又会调用 DAL 的 Delete 方法,从而使相应的 DELETE 语句发送到数据库。之后,数据重新绑定到 GridView ,标记回送至客户端,该测试分类就不再显示出来了。

尽管该删除过程从 Categories 表中成功地删除了该测试分类记录,却并未从 web 服务器的文件系统中删除其小册子文件。刷新 Solution Explorer 可以看到 Test.pdf 仍然存在于 ~/Brochures 文件夹下。

图8 :Test.pdf 文件未从 web 服务器的文件系统中删除

步骤5 : 删除已删除分类的小册子文件

在数据库外保存二进制数据的一个不利之处在于 , 当删除某条数据库记录时 , 必须采取额外的步骤来清除相关的文件。在 GridView 和 ObjectDataSource 中有两个事件,它们分别在删除命令执行的前后触发。实际上我们需要为这两个分别在操作前后触发的事件创建 event handler 。在删除 Categories 记录之前,我们需要确定其 PDF 文件的路径,但我们不想在删除分类之前删除该 PDF 文件,原因是,万一发生某种意外使分类没有得以删除,就不用删除该文件了。

GridView 的 RowDeleting 事件 在调用 ObjectDataSource 的删除命令之前触发 , 而其 RowDeleted 事件 在该调用之后触发。为这两个事件创建 event handler ,代码如下:

// 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 event handler 中 , 从 GridView 的DataKeys 集合中获取要删除的行的 CategoryID , 在本event handler 中可以通过 e.Keys 集合来访问该 DataKeys 集合。然后,调用 CategoriesBLL 类的 GetCategoryByCategoryID(categoryID) 来返回要删除记录的有关信息。如果返回的 CategoriesDataRow 对象有非空的 BrochurePath 值,则将该值保存于页面变量 deletedCategorysPdfPath 中以便在 RowDeleted event handler 中可以删除相应文件。

注意 : 相对于在 RowDeleting event handler 中检索要删除的 Categories 记录的 BrochurePath 细节 , 我们还可以选择将 BrochurePath 添加到 GridView 的 DataKeyNames 属性并通过 e.Keys 集合访问该记录的值。这样做会稍许增加 GridView 的视图的状态大小,但会减少所需代码数量并节省了一次对数据库的数据往返。

在调用ObjectDataSource 的 底层删除命令后 , 系统会触发 GridView 的 RowDeleted event handler 。如果在删除数据时没有出现异常情况 , 并且deletedCategorysPdfPath 有一非空值 , 则从文件系统中删除相应的PDF 文件。请注意,在删除该分类的图片的相关二进制数据时不需要该额外代码。其原因是,图片数据是直接保存于数据库中的,删除 Categories 行的同时会删除分类的图片数据。

在添加这两个event handler 后 , 再次执行此测试用例。在删除分类时 , 其相关PDF 也被删除。

更新现有记录的相关二进制数据是一个有趣的挑战。本教程的剩余部分我们将探讨怎样添加针对小册子和图片的更新功能。步骤 6 探讨更新小册子信息的技巧,步骤 7 探讨怎样更新图片。

步骤6 : 更新分类的小册子

在 数据插入、更新和删除概述 教程中我们讲过 ,GridView 提供有内置的行级编辑支持 , 如果其底层数据源配置合适 , 则通过选中一个复选框既可实现该功能。目前, CategoriesDataSource ObjectDataSource 的配置尚不支持更新,因此我们在这里添加进来。

在 ObjectDataSource 的向导中单击 Configure Data Source 链接并进行到第二步。由于 CategoriesBLL 中使用了 DataObjectMethodAttribute ,在 UPDATE 的下拉列表中会自动填充有 UpdateCategory 重载方法,该方法包含四个输入参数(只是不包含针对 Picture 列的参数)。对此进行更改,使其使用带有五个参数的重载方法。

图9:配置 ObjectDataSource , 使其使用含有 Picture 参数的 UpdateCategory 方法

现在 ,ObjectDataSource 包含一个 UpdateMethod 属性值以及相应的 UpdateParameters 。如步骤 4 中所述 , 在使用 Configure Data Source 向导时 ,Visual Studio 将 ObjectDataSource 的OldValuesParameterFormatString 属性设置为original_{0} 。而这在进行更新与删除方法的调用时会引发问题。因此,或者彻底清除该属性,或者将其重新设置为默认值 {0}。

在结束该向导并修改OldValuesParameterFormatString 之后 ,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 的智能标记中的 “Enable Editing” 选项以打开 GridView 的内置编辑特性。此操作将 CommandField 的 ShowEditButton 属性设置为 True , 其结果是一个Edit 按钮加入进来 ( 要编辑的行处于编辑状态时 ,Update 和 Cancel 按钮会呈现出来 ) 。

图10 : 配置 GridView 使其支持编辑

通过一个浏览器访问该页面 , 然后单击其中一行的 Edit 按钮。两个 BoundField , CategoryName 和 Description ,都呈现为文本框。 BrochurePath TemplateField 缺少 EditItemTemplate ,因此它仍然呈现为 ItemTemplate – 一个指向小册子的链接。 Picture ImageField 呈现为文本框,其 Text 属性被赋值为 ImageField 的 DataImageUrlField 值,此处为 CategoryID 。

图11 :GridView 没有针对 BrochurePath 的编辑界面

定制BrochurePath 的编辑界面

我们需要为BrochurePath TemplateField 创建一个编辑界面 , 该界面允许用户 :

  • 保持分类的小册子原样不变,
  • 上传一个新的小册子来更新分类的原有小册子 , 或
  • 彻底删除分类的小册子 ( 在分类不再有相关小册子的情况下 ) 。

我们还需要更新 Picture ImageField 的编辑界面 , 但我们会在步骤 7 中才进行该项工作。

在GridView 的智能标记中 ,单击 “Edit Templates” 链接 , 从下拉列表中选择 BrochurePath TemplateField 的EditItemTemplate 。将一个 RadioButtonList Web 控件添加到该模板中,将其 ID 属性设置为 BrochureOptions ,其 AutoPostBack 属性设置为 True 。在 Properties 窗口中 , 单击Items 属性中的省略号 , 此操作会启动 ListItem Collection Editor 。添加以下三个选项,使其 Value 值分别为 1 、 2 和 3 :

  • Use current brochure
  • Remove current brochure
  • Upload new brochure

将第一个 ListItem 的 Selected 属性设置为 True 。

图12 : 在 RadioButtonList 中添加三个 ListItem

在RadioButtonList 下添加一个名为 BrochureUpload 的FileUpload 的控件。 . 将其 Visible 属性设置为 False 。

图13 : 在 EditItemTemplate 中添加 RadioButtonList 和 FileUpload 控件

此 RadioButtonList 为用户提供 3 个选项。我们的想法是这样的 : 只有在选择了最后一个选项 “Upload new brochure” 时 ,FileUpload 控件才会显示出来。为此,我们为 RadioButtonList 的 SelectedIndexChanged 事件创建一个 event handler 并添加以下代码:

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 控件在同一个模板中 , 我们需要通过编程来访问这些控件。系统通过 sender 输入参数将一个对 RadioButtonList 的引用传递给了 SelectedIndexChanged event handler 。为了得到 FileUpload 控件,我们需要得到 RadioButtonList 的父控件并在其中使用 FindControl("controlID") 方法。一旦我们拥有了对 RadioButtonList 和 FileUpload 这两个控件的一个引用,就可以实现这一点了:只有当 RadioButtonList 的 SelectedValue 等于 3 (此为 “Upload new brochure” ListItem 的 Value 的值)时,才将 FileUpload 控件的 Visible 属性设置为 True 。

准备好该代码后 , 我们花点时间来对该编辑界面进行测试。单击一行的 Edit 按扭。开始时默认选择的是 “Use current brochure” 选项。改选另一项会引发一次回传。如果选择第三个选项, FileUpload 控件会显示出来,否则,该控件处于隐藏状态。图 14 所示为首次单击 Edit 按扭时出现的编辑界面,图 15 所示为选择 “Upload new brochure” 选项时的界面。

图14 : 开始时选择的是 “Use current brochure” 选项

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

保存小册子文件并更新BrochurePath 列

单击GridView 的 Update 按扭会触发其 RowUpdating 事件。接着 , 系统调用ObjectDataSource 的更新命令 , 然后触发 GridView 的 RowUpdated 事件。类似于删除过程,我们需要为这两个事件创建 event handler 。在 RowUpdating event handler 中,我们需要根据 BrochureOptions RadioButtonList 的 SelectedValue 来决定采取何种操作:

  • 如果 SelectedValue 是 “1” , 我们要继续使用相同的 BrochurePath 设置。因此 , 我们需要将 ObjectDataSource 的 brochurePath 参数设置为要更新的记录的当前的BrochurePath 值。可以用以下语句来设置 ObjectDataSource 的 brochurePath 参数: e.NewValues["brochurePath"] = value
  • 如果 SelectedValue 为 “2” , 我们要将该记录的 BrochurePath 值设置为 NULL 。可以这样来实现 : 将 ObjectDataSource 的 brochurePath 参数设置为 Nothing , 其结果是 , 在 UPDATE 语句中会使用数据库的 NULL 值。如果有一个现有的小册子文件要删除,我们就需要删除该现有文件,但是,我们只想在更新完成而没有异常情况出现时才删除该文件。
  • 如果 SelectedValue 为 “3” , 我们要确保用户上传了一个 PDF 文件 , 然后将该文件保存到文件系统中并更新该记录的BrochurePath 列值。另外,如果当前已有一个要更换的小册子文件,我们需要删除该原先的文件。但是,我们只想在更新完成而没有异常情况出现时才删除该文件。

当RadioButtonList 的 SelectedValue 为“3” 时需要完成的步骤实际上就是 DetailsView 的 ItemInserting event handler 所用的那些步骤。当从 DetailsView 控件(我们在 上一篇教程 中添加的控件)添加一条新的分类记录时会执行该 event handler 。因此,我们可将此功能重构到不同的方法中。具体地,我将共同的功能转移到了两个方法中:

  • ProcessBrochureUpload(FileUpload, outbool) – 以一个 FileUpload 控件实例作为输入参数 , 结果为一个布尔值 , 该布尔值指示是应该继续该删除或编辑操作 , 还是应该取消该操作 ( 因某种验证问题 ) 。此方法返回保存文件的路径,或者,如果没有保存文件,返回空值。
  • DeleteRememberedBrochurePath – 如果 deletedCategorysPdfPath 非空 , 删除页面变量 deletedCategorysPdfPath 中的路径所指定的文件。

下面是这两个方法的代码。请注意ProcessBrochureUpload 和上一个教程的DetailsView 的 ItemInserting event handler 之间有某些相似性。在本教程中,我更新了 DetailsView 的 event handler 以使用这里的新方法。请下载本教程的有关代码以查看对 DetailsView 的 event handler 的修改。

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 event handler 使用 ProcessBrochureUpload 和DeleteRememberedBrochurePath 方法 , 如下代码所示 :

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 event handler 使用了一系列的条件语句 , 以便根据 BrochureOptions RadioButtonList 的 SelectedValue 属性值采取相应的操作。 .

准备好该代码后,您可以编辑一个分类,然后试着令该分类使用其当前的小册子、不使用小册子,以及上传一个新的小册子。Go ahead and try it out. 在 RowUpdating 和 RowUpdated event handler 中设置一些断点以感受一下其工作流程。

步骤7 : 上传新图片

Picture ImageField 的编辑界面显示为一个文件框 , 该文本框中填充有 DataImageUrlField 属性值。在编辑流程中, GridView 将一个参数传递给 ObjectDataSource ,该参数的名称为 ImageField 的 DataImageUrlField 属性的值,该参数的值为编辑界面输入到该文本框中的值。当图像作为文件保存到文件系统中并且 DataImageUrlField 包含图象完整的 URL 时,这种行为是合适的。这种情况下,编辑界面在文本框中显示图像的 URL ,用户可以更改该 URL 并将其保存回到数据库中。虽然该默认界面不允许用户上传新的图像,但它确实允许用户将图像的 URL 从当前值改成另一值。然而,对于本教程, ImageField 的默认编辑界面不能满足需要,因为 Picture 的二进制数据直接保存于数据库中而且 DataImageUrlField 属性只含有 CategoryID 。

为了更好地理解当用户编辑一行中的 ImageField 时在我们的教程中的代码发生了什么情况,请观察以下例子: 用户对CategoryID 为 10 的行进行编辑 , 该操作使 Picture ImageField 显示为一个值为 “10” 的文本框。设想用户将此文本框中的值改为 “50” 后单击 Update 按扭。此时会发生一次回传,并且 GridView 最初创建一个名为 CategoryID 值为 “50” 的参数。然而,在 GridView 发送此参数(以及 CategoryName 和 Description 参数)之前,它将 DataKeys 集合中的值添加进来。因此 ,GridView 以当前行的底层 CategoryID 值 , 也就是 10 , 覆盖了CategoryID 参数。简言之, ImageField 的编辑界面对此教程中的编辑流程没有任何影响,其原因是 ImageField 的 DataImageUrlField 属性与 grid 的 DataKey 值是同一个名称。

ImageField 易于根据数据库数据来显示图像。不过我们不想在编辑界面上提供一个文本框。我们想提供一个 FileUpload 控件,终端用户可使用该控件来更改分类的图片。与 BrochurePath 值不同的是,我们决定在这些教程中要求每个分类必须有一个图片。因此,我们不需要让用户指出没有相关图片 – 用户要么上传一个新图片要么保持当前图片原样不变。

为了定制ImageField 的编辑界面 , 我们需要将其转换为一个TemplateField 。在 GridView 的智能标记上 , 单击“Edit Columns” 链接 , 选择 ImageField 后单击 “Convert this field into a TemplateField” 链接。

图16 : 将 ImageField 转换为 TemplateField

以这种方式将ImageField 转换为 TemplateField 会生成一个有两个模板的 TemplateField 。如下面的声明式语法所示, ItemTemplate 模板包含一个 Image Web 控件,用绑定语法根据 ImageField 的 DataImageUrlField 属性和 DataImageUrlFormatString 属性对该控件的 ImageUrl 属性进行了赋值。 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 的 智能标记上 , 单击“Edit Templates” 链接 , 然后从下拉列表中选择 Picture TemplateField 的 EditItemTemplate 。在该模板中,您会看到一个 TextBox – 请删除它。然后,将一个 FileUpload 控件从 Toolbox 中拖放到模板中,将其 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 按扭时,图片列会显示出一段文本和一个 FileUpload 控件。

图18 :Editing 界面含有一个 FileUpload 控件

我们记得 ,ObjectDataSource 配置为可调用 CategoriesBLL 类的 UpdateCategory 方法 , 该方法以一个字节数组来接受输入的图片二进制数据。然而,如果此数组为空值,系统会调用另一个重载的 UpdateCategory 方法,该方法会发出一个不对 Picture 列进行修改的 UPDATE SQL 语句,因此会原封不动地保持该分类的当前图片不变。因此,在 GridView 的 RowUpdating event handler 中,我们需要编程引用名为 PictureUpload 的 FileUpload 控件并确定是否上传了文件。如果没有上传文件,则我们不想为 picture 参数指定一值。反之,如果在 PictureUpload FileUpload 控件中上传了一个文件,我们想确保该文件为 JPG 文件。如果是 JPG 文件,则我们可以将其二进制内容通过 picture 参数传给 ObjectDataSource 。

类似于步骤 6 中使用的代码,这里需要的代码大多数已存在于 DetailsView 的 ItemInserting event handler 中了。因此,我已将共用的功能重构为一个新的方法, ValidPictureUpload ,并且更新了 ItemInserting event handler 以使用此方法。

在 GridView 的 RowUpdating event handler 的开头部分添加如下代码。将此代码放于保存小册子的代码之前是很重要的 , 因为我们不想在上传的图片无效时还将小册子保存到web 服务器的文件系统中。

// 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 参数不会被设置,因此会使用其默认的空值。如果上传了一个文件且 ValidPictureUpload 返回为 True , picture 参数会赋值为上传的图像的二进制数据;如果该方法返回为 False ,则取消更新流程,并退出 event handler 。

如下是ValidPictureUpload(FileUpload) 方法的代码 , 该代码是从DetailsView 的 ItemInserting event handler 重构而来 :

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 文件更换原始分类的图片

回忆一下 , 原先的八个分类的图片是封装有 OLE 报头的位图文件。既然我们已添加了对现有记录的图片进行编辑的功能,就花些时间来将这些位图文件更换为 JPG 文件吧。如果想继续使用当前的分类图片,可以通过以下步骤将它们转换成 JPG 文件:

  1. 将位图图像保存到硬盘。在浏览器中访问UpdatingAndDeleting.aspx 页面 , 对于前八个分类中的每一个分类 , 右键单击其图像并选择保存图片。
  2. 在所选图像编辑器中打开该图像。例如 , 您可以选择使用 Microsoft Paint 。
  3. 将位图图像保存为 JPG 图像。
  4. 通过编辑界面 , 用该 JPG 文件更新该分类的图片。

在编辑完分类并上传其 JPG 图像后 , 该图像不会呈示在浏览器中 , 因为DisplayCategoryPicture.aspx 页面要从前八个分类的图片中剥离出前78 个字节。通过删除执行 OLE 头剥离任务的代码可改正这点。修改之后, DisplayCategoryPicture.aspx 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]; 
 
    // 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 不允许空值,应添加 RequiredFieldValidator 。 Description TextBox 可能应该转换为多行 TextBox 。我将这些结尾的小修改留给读者作为练习。

小结

本教程介绍怎样处理二进制数据的最后一篇教程。在本教程和前面的三篇教程中,我们了解到怎样将二进制数据存贮到文件系统中或直接存贮到数据库中。用户通过选择硬盘中的文件并将其上传到 web 服务器来向系统提供二进制数据,数据可以保存到服务器的文件系统中或插入到数据库中。 ASP.NET 2.0 含有一个 FileUpload 控件,该控件的存在使得仅通过拖放既可轻松地提供这样一个界面。然而,如上载文件教程所述, FileUpload 控件只适用于相对较小的文件的上传,理论上文件的字节数不超过一兆。我们探讨了怎样从现有记录中编辑和删除二进制数据,并且还探讨了怎样将上传的数据与底层数据模型相关联。

我们在下一系列的教程中将探讨各种高速缓存技术。高速缓存提供了一个改进应用程序整体性能的方法,它将通过昂贵的操作所取得的结果存贮到一个可以更快地访问的位置。

快乐编程!

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