上载和删除现有的二进制数据 (C#)

作者 :斯科特·米切尔

下载 PDF

在前面的教程中,我们了解 GridView 控件如何使编辑和删除文本数据变得简单。 在本教程中,我们将了解 GridView 控件如何使编辑和删除二进制数据(无论是保存在数据库中还是存储在文件系统中)。

介绍

在过去的三个教程中,我们添加了相当多的功能,用于处理二进制数据。 我们首先在Categories表中添加BrochurePath列,并相应地更新了体系结构。 我们还添加了数据访问层和业务逻辑层方法,用于处理“类别”表的现有 Picture 列,该列包含图像文件的二进制内容。 我们构建了网页,用于在 GridView 中将二进制数据呈现为小册子的下载链接,同时在 <img> 元素中显示类别图片,并添加了 DetailsView,以允许用户添加新类别并上传其小册子和图片数据。

仍要实现的所有作都是编辑和删除现有类别的功能,我们将在本教程中使用 GridView 的内置编辑和删除功能完成此作。 编辑类别时,用户可以选择上传新图片或让该类别继续使用现有图片。 对于该小册子,他们可以选择使用现有小册子、上传新小册子或指示该类别不再具有与之关联的小册子。 让我们开始吧!

步骤 1:更新数据访问层

DAL 具有自动生成InsertUpdate的方法和Delete方法,但这些方法是基于CategoriesTableAdapter主查询生成的,不包括Picture列。 因此, InsertUpdate 方法不包括用于指定类别图片的二进制数据的参数。 与 在前面的教程中一样,我们需要创建一个新的 TableAdapter 方法,用于在指定二进制数据时更新 Categories 表。

打开类型化数据集,然后在设计器中右键单击 CategoriesTableAdapter s 标头,然后从上下文菜单中选择“添加查询”以启动 TableAdapter 查询配置向导。 此向导首先询问 TableAdapter 查询应如何访问数据库。 选择“使用 SQL 语句”,然后单击“下一步”。 下一步将提示生成查询的类型。 由于我们正在创建查询以向表添加新记录 Categories ,请选择“更新”,然后单击“下一步”。

选择 UPDATE 选项

图 1:选择 UPDATE 选项(单击以查看全尺寸图像

现在需要指定 UPDATE SQL 语句。 向导会自动建议一条与 TableAdapter 主查询相对应的语句(用于更新 CategoryNameDescriptionBrochurePath 值)。 更改语句,以便将 Picture 列和 @Picture 参数一起包含,如下所示:

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

向导的最后一个屏幕要求我们命名新的 TableAdapter 方法。 输入 UpdateWithPicture 并单击“完成”。

将 New TableAdapter 方法命名为 UpdateWithPicture

图 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;
}

在本教程中,让我们创建两个方法来更新类别——一个方法需要二进制图片数据,并调用我们刚刚添加到 CategoriesTableAdapterUpdateWithPicture 方法,另一个方法只需接受 CategoryNameDescriptionBrochurePath 的值,并使用 CategoriesTableAdapter 类自动生成的 Update 语句。 使用两种方法背后的理由是,在某些情况下,用户可能需要更新类别的图片及其其他字段,在这种情况下,用户必须上传新图片。 然后,UPDATE 语句中可以使用上传的图片的二进制数据。 在其他情况下,用户可能只对更新名称和说明感兴趣。 但是,如果 UPDATE 语句也期望 Picture 列的二进制数据,那么我们也需要提供这些信息。 这需要额外访问数据库,以便为正在编辑的记录获取图片数据。 因此,我们需要两 UPDATE 种方法。 业务逻辑层将根据是否在更新类别时提供图片数据来确定使用哪种数据。

为此,请将两个名为UpdateCategory的方法添加到CategoriesBLL类中。 第一个应接受三个string,一个byte数组,以及一个int作为其输入参数。第二个只有三个string和一个intstring输入参数用于类别的名称、说明和小册子文件路径,byte数组用于类别图片的二进制内容,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工作,不如将本教程的更改放在同一文件夹中~/BinaryDataUpdatingAndDeleting.aspx页面中。 将声明性标记和代码从 UploadInDetailsView.aspx 中复制并粘贴到 UpdatingAndDeleting.aspx

首先打开 UploadInDetailsView.aspx 页面。 复制元素中的所有 <asp:Content> 声明性语法,如图 3 所示。 接下来,打开 UpdatingAndDeleting.aspx 此标记并将其粘贴到其 <asp:Content> 元素中。 同样,将页面代码隐藏类中的 UploadInDetailsView.aspx 代码复制到 UpdatingAndDeleting.aspx.

从UploadInDetailsView.aspx复制声明性标记

图 3:复制声明性标记(UploadInDetailsView.aspx单击以查看全尺寸图像

复制声明性标记和代码后,请访问 UpdatingAndDeleting.aspx。 应会看到相同的输出,并且具有与 UploadInDetailsView.aspx 上一教程中的页面相同的用户体验。

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

正如我们在 “插入、更新和删除数据概述” 教程中所述,如果网格的基础数据源支持删除,则 GridView 提供了内置删除功能,并且如果网格的基础数据源支持删除,则可以在复选框滴答声中启用这些功能。 目前,GridView 绑定到CategoriesDataSource的 ObjectDataSource 不支持删除。

若要解决此问题,请单击 ObjectDataSource 智能标记中的“配置数据源”选项以启动向导。 第一个屏幕显示 ObjectDataSource 配置为使用 CategoriesBLL 类。 点击下一步。 目前,仅指定 ObjectDataSource 和InsertMethodSelectMethod属性。 但是,向导会分别使用 UpdateCategoryDeleteCategory 方法自动填充 UPDATE 和 DELETE 选项卡中的下拉列表。 在CategoriesBLL类中,这是因为我们使用DataObjectMethodAttribute将这些方法标记为更新和删除的默认方法。

目前,将 UPDATE 选项卡的下拉列表设置为 “无”,但将 DELETE 选项卡的下拉列表设置为 DeleteCategory。 我们将在步骤 6 中返回到此向导,添加更新支持。

将 ObjectDataSource 配置为使用 DeleteCategory 方法

图 4:将 ObjectDataSource 配置为使用 DeleteCategory 方法(单击以查看全尺寸图像

注释

完成向导后,Visual Studio 可能会询问是否要刷新字段和密钥,这将重新生成数据 Web 控件字段。 选择“否”,因为选择“是”将覆盖你可能所做的任何字段自定义。

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 智能标记中的“启用删除”复选框,将删除功能添加到 GridView。 这将向 GridView 添加一个 CommandField,其 ShowDeleteButton 属性设置为 true.

在 GridView 中启用对删除的支持

图 5:启用对在 GridView 中删除的支持(单击以查看全尺寸图像

花点时间测试删除功能。 在ProductsCategoryIDCategoriesCategoryID之间有外键,因此,如果尝试删除前八个类别中的任何一个,则会出现外键约束冲突异常。 若要测试此功能,请添加新类别,同时提供小册子和图片。 图 6 中显示的测试类别包括名为 Test.pdf 的测试手册文件和测试图片。 添加测试类别后,图 7 显示了 GridView 的状态。

使用小册子和图像添加测试类别

图 6:使用小册子和图像添加测试类别(单击可查看全尺寸图像

插入测试类别后,它将显示在 GridView 中

图 7:插入测试类别后,它将显示在 GridView 中(单击以查看全尺寸图像

在 Visual Studio 中,刷新解决方案资源管理器。 现在应会看到文件夹中的新文件 ~/BrochuresTest.pdf 请参阅图 8)。

接下来,单击“测试类别”行中的“删除”链接,使页面回到原始状态,并触发类CategoriesBLLDeleteCategory方法被调用。 这将调用 DAL 方法 Delete ,导致相应的 DELETE 语句发送到数据库。 然后,数据会反弹到 GridView,并将标记发送回客户端,且测试类别不再存在。

虽然删除工作流成功地从 Categories 表中删除了测试类别记录,但它并没有从 Web 服务器文件系统中删除其小册子文件。 刷新解决方案资源管理器,你会看到 Test.pdf 仍然存在于 ~/Brochures 文件夹中。

未从 Web 服务器文件系统中删除 Test.pdf 文件

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

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

在数据库外部存储二进制数据的缺点之一是,删除关联的数据库记录时,必须执行额外的步骤来清理这些文件。 GridView 和 ObjectDataSource 提供在执行 delete 命令之前和之后触发的事件。 我们实际上需要为“动作前”和“动作后”事件创建事件处理程序。 Categories在删除记录之前,我们需要确定其 PDF 文件的路径,但在删除该类别之前,我们不希望删除 PDF,以防有一些异常,并且不会删除该类别。

GridView 的事件RowDeleting在调用 ObjectDataSource 的删除命令之前触发,而其RowDeleted事件在之后触发。 使用以下代码为这两个事件创建事件处理程序:

// 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 事件处理程序中,通过 e.Keys 集合获取要删除的行的 CategoryID,该行来自 GridView 的 DataKeys 集合,您可以在此事件处理程序中访问该集合。 接下来,调用 CategoriesBLLGetCategoryByCategoryID(categoryID) 来返回有关要删除的记录的信息。 如果返回 CategoriesDataRow 的对象具有非NULL``BrochurePath 值,则会将其存储在页变量 deletedCategorysPdfPath 中,以便可以在事件处理程序中删除 RowDeleted 该文件。

注释

与其在RowDeleting事件处理程序中检索要删除的记录BrochurePath的详细信息Categories,我们可以选择将BrochurePath属性添加到GridView的DataKeyNames属性中,并通过e.Keys集合访问记录的值。 这样做会稍微增加 GridView 的视图状态大小,但会减少所需的代码量,并避免数据库查询。

调用 ObjectDataSource 基础删除命令后,GridView 的 RowDeleted 事件处理程序将触发。 如果删除数据时没有异常,并且存在一个值 deletedCategorysPdfPath,则会从文件系统中删除 PDF。 请注意,不需要此额外代码来清理与其图片关联的类别二进制数据。 这是因为图片数据直接存储在数据库中,因此删除 Categories 该行也会删除该类别的图片数据。

添加两个事件处理程序后,再次运行此测试用例。 删除类别时,也会删除其关联的 PDF。

更新现有记录的关联二进制数据会带来一些有趣的挑战。 本教程的其余部分将深入探讨如何向小册子和图片添加更新功能。 步骤 6 探索更新小册子信息的技术,而步骤 7 则着眼于更新图片。

步骤 6:更新类别小册子

“插入、更新和删除数据概述”教程中所述,GridView 提供内建的行级编辑支持,如果基础数据源配置得当,仅需简单勾选复选框即可实现。 目前, CategoriesDataSource ObjectDataSource 尚未配置为包含更新支持,因此让我们添加它。

单击 ObjectDataSource 向导中的“配置数据源”链接,然后继续执行第二步。 由于使用了DataObjectMethodAttribute中的CategoriesBLL,UPDATE 下拉列表应会自动填充接受四个输入参数的UpdateCategory重载,适用于除Picture列之外的所有列。 更改此项,使其使用具有五个参数的重载。

将 ObjectDataSource 配置为使用包含图片参数的 UpdateCategory 方法

图 9:将 ObjectDataSource 配置为使用 UpdateCategory 包含参数 Picture 的方法(单击以查看全尺寸图像

ObjectDataSource 现在将包含其 UpdateMethod 属性的值以及对应的 UpdateParameter 值。 如步骤 4 中所述,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 的内置编辑功能,请检查 GridView 智能标记中的“启用编辑”选项。 这将将 CommandField s ShowEditButton 属性设置为 true,从而添加编辑按钮(以及正在编辑的行的“更新和取消”按钮)。

配置 GridView 以支持编辑

图 10:将 GridView 配置为支持编辑(单击以查看全尺寸图像

通过浏览器访问页面,然后单击其中一行的“编辑”按钮。 CategoryNameDescription BoundFields 被呈现为文本框。 BrochurePath TemplateField 缺少一个EditItemTemplate,因此它继续显示其ItemTemplate与小册子的链接。 Picture ImageField 呈现为 TextBox,其Text属性被设为 ImageField 的DataImageUrlField值,在本例中为CategoryID

GridView 缺少适用于 BrochurePath 的编辑界面

图 11:GridView 缺少编辑界面(BrochurePath单击以查看全尺寸图像

BrochurePath自定义 s 编辑界面

我们需要为 BrochurePath TemplateField 创建一个编辑界面,该界面允许用户执行以下作之一:

  • 离开该类别的小册子 as-is,
  • 通过上传新小册子来更新类别小册子,或
  • 在类别不再有对应小册子的情况下,完全删除该类别的小册子。

我们还需要更新 Picture ImageField 的编辑界面,但我们将在步骤 7 中完成此作。

在 GridView 的智能标记里,单击“编辑模板”链接,然后从下拉列表中选择 BrochurePath 模板字段EditItemTemplate。 向此模板添加 RadioButtonList Web 控件,将其 ID 属性设置为 BrochureOptions ,并将属性 AutoPostBack 设置为 true。 在“属性”窗口中,单击属性中的 Items 省略号,这将打开 ListItem 集合编辑器。 分别添加以下三个选项,其中包含 Value 1、2 和 3:

  • 请使用现行小册子
  • 删除当前小册子
  • 上传新小册子

将第一个 ListItem 属性 Selected 设置为 true.

将三个列表项添加到单选按钮列表中

图 12:向单选按钮列表添加三个 ListItem s

在 RadioButtonList 下,添加名为 的 BrochureUploadFileUpload 控件。 将其 Visible 属性设置为 false

将 RadioButtonList 和 FileUpload 控件添加到 EditItemTemplate

图 13:向EditItemTemplate添加 RadioButtonList 和 FileUpload 控件(单击以查看全尺寸图像

此 RadioButtonList 为用户提供了三个选项。 其想法是,仅当选择了最后一个选项“上传新小册子”时,才会显示 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 控件在模板中,因此我们必须编写一些代码以编程方式访问这些控件。 事件处理程序 SelectedIndexChanged 会在输入参数 sender 中传入一个指向 RadioButtonList 的引用。 要获取 FileUpload 控件,我们需要获取 RadioButtonList 的父控件,并在父控件上使用 FindControl("controlID") 方法。 一旦我们获得了对 RadioButtonList 和 FileUpload 控件的引用,只有当 RadioButtonList 的 SelectedValue 等于 3 时,FileUpload 控件的Visible属性才会设置为true,这正是用于上传新宣传册的Value

完成此代码后,请花点时间测试编辑界面。 单击针对某行的“编辑”按钮。 最初,应选择“使用当前小册子”选项。 更改选定的索引会触发回发。 如果选择第三个选项,则显示 FileUpload 控件,否则将隐藏它。 图 14 显示首次单击“编辑”按钮时的编辑界面;图 15 显示选择“上传新小册子”选项后界面。

最初,已选择“使用当前小册子选项”

图 14:最初,已选择“使用当前小册子选项”(单击以查看全尺寸图像

选择“上传新小册子”选项显示 FileUpload 控件

图 15:选择“上传新小册子选项显示 FileUpload 控件”(单击以查看全尺寸图像

保存手册文件并更新BrochurePath

单击 GridView 的“更新”按钮时,其 RowUpdating 事件将触发。 调用 ObjectDataSource 的 update 命令后,GridView 的 RowUpdated 事件被触发。 与删除工作流一样,我们需要为这两个事件创建事件处理程序。 在 RowUpdating 事件处理程序中,我们需要基于 SelectedValueBrochureOptions RadioButtonList,确定要执行的操作。

  • 如果 SelectedValue 是 1,我们希望继续使用同一 BrochurePath 设置。 因此,我们需要将 ObjectDataSource s brochurePath 参数设置为要更新的记录的现有 BrochurePath 值。 可以使用e.NewValues["brochurePath"] = value设置ObjectDataSource的brochurePath参数。
  • 如果为 SelectedValue 2,则我们希望将记录 BrochurePath 的值设置为 NULL。 这可以通过将 ObjectDataSource s brochurePath 参数设置为Nothing来实现,这会导致在UPDATE语句中使用数据库NULL。 如果存在正在删除的现有小册子文件,则需要删除现有文件。 但是,仅当更新完成而不引发异常时,我们才想要执行此作。
  • 如果为 SelectedValue 3,则我们希望确保用户已上传 PDF 文件,然后将其保存到文件系统并更新记录的 BrochurePath 列值。 此外,如果有正在替换的现有小册子文件,则需要删除以前的文件。 但是,仅当更新完成而不引发异常时,我们才想要执行此作。

当 RadioButtonList s 为 SelectedValue 3 时完成的步骤与 DetailsView 事件处理程序 ItemInserting 所使用的步骤几乎相同。 从 我们在上一教程中添加的 DetailsView 控件添加新的类别记录时,将执行此事件处理程序。 因此,我们有必要将此功能分解为单独的方法。 具体而言,我已将通用功能移出两种方法:

  • ProcessBrochureUpload(FileUpload, out bool) 接受一个 FileUpload 控件实例,并返回一个布尔值,该值指定删除或编辑操作是否应继续,还是由于某些验证错误而取消。 此方法返回已保存文件的路径;如果未保存任何文件,则返回 null
  • DeleteRememberedBrochurePath如果不是null,则删除页面变量deletedCategorysPdfPathdeletedCategorysPdfPath中路径指定的文件。

这两种方法的代码如下。 请注意本教程中 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 和RowUpdatingRowUpdated事件处理程序使用ProcessBrochureUploadDeleteRememberedBrochurePath方法,如以下代码所示:

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 一系列条件语句根据 BrochureOptions RadioButtonList 的 SelectedValue 属性值执行相应的作。

有了这段代码,你可以编辑一个类别,并选择使用当前的小册子、不使用小册子或上传新的小册子。 继续尝试。在事件处理程序中RowUpdatingRowUpdated设置断点以了解工作流。

步骤 7:上传新图片

Picture ImageField 的编辑界面呈现为填充其DataImageUrlField属性中的值的文本框。 在编辑工作流期间,GridView 将一个参数传递给 ObjectDataSource,参数名称是 ImageField 的DataImageUrlField属性值,参数值是编辑界面中文本框中输入的值。 当图像保存为文件系统上的文件并且 DataImageUrlField 包含图像的完整 URL 时,此行为适用。 在这种情况下,编辑界面会在文本框中显示图像的 URL,用户可以更改图像并将其保存回数据库。 诚然,此默认接口不允许用户上传新图像,但确实允许他们将图像的 URL 从当前值更改为另一个值。 但是,对于本教程而言,ImageField 的默认编辑接口是不够的,因为 Picture 二进制数据直接存储在数据库中,而 DataImageUrlField 属性仅持有 CategoryID

若要更好地了解当用户使用 ImageField 编辑行时所发生的情况,请考虑以下示例:用户编辑包含 CategoryID 10 的行,导致 Picture ImageField 呈现为值为 10 的文本框。 假设用户将此文本框中的值更改为 50,然后单击“更新”按钮。 发生回发,GridView 最初创建一个名为 CategoryID 的参数,其值为 50。 但是,在 GridView 发送此参数(以及CategoryNameDescription参数)之前,它会先从DataKeys集合中添加值。 因此,它会将CategoryID参数用当前行的底层CategoryID值10覆盖。 简言之,ImageField 的编辑界面不会影响本教程的编辑工作流,因为 ImageField 属性 DataImageUrlField 的名称和网格 DataKey 值相同。

虽然 ImageField 可以轻松地基于数据库数据显示图像,但我们不希望在编辑界面中提供文本框。 相反,我们希望提供一个 FileUpload 控件,最终用户可以使用该控件来更改类别的图片。 与BrochurePath值不同,在这些教程中,我们决定要求每个类别必须要有图片。 因此,我们不需要让用户指示没有关联的图片,用户可以上传新图片或者保留当前图片 as-is。

若要自定义 ImageField 的编辑界面,需要将其转换为 TemplateField。 在 GridView 智能标记中,单击“编辑列”链接,选择 ImageField,然后单击“将此字段转换为 TemplateField”链接。

将 ImageField 转换为 TemplateField

图 16:将 ImageField 转换为 TemplateField

以这种方式将 ImageField 转换为 TemplateField 会生成包含两个模板的 TemplateField。 如以下声明性语法所示,包含一个图像 Web 控件,ItemTemplateImageUrl控件使用基于 ImageField 和DataImageUrlFieldDataImageUrlFormatString属性的数据绑定语法分配其属性。 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 s EditItemTemplate 。 在模板中,应会看到一个 TextBox 将其删除。 接下来,将 FileUpload 控件从工具箱拖动到模板中,将其 ID 设置为 PictureUpload。 此外,添加以下文本:要更改类别的图片,请指定一张新图片。 若要使类别的图片保持不变,请在模板中将字段留空。

将 FileUpload 控件添加到 EditItemTemplate

图 17:向EditItemTemplate添加 FileUpload 控件 (单击以查看全尺寸图像

自定义编辑界面后,在浏览器中查看进度。 在只读模式下查看行时,类别的图像显示为与以前相同,但单击“编辑”按钮后,图片列将变为文本,并附带一个文件上传控件。

编辑界面包括 FileUpload 控件

图 18:编辑界面包含 FileUpload 控件(单击以查看全尺寸图像

回想一下,ObjectDataSource 被配置为调用 CategoriesBLL 类的 UpdateCategory 方法,该方法以 byte 数组的形式接受图片的二进制数据作为输入。 如果此数组具有 null 的值,则会调用备用 UpdateCategory 重载,它会发出一个不修改 Picture 列的 UPDATE SQL 语句,从而保留类别的当前图片。 因此,在 GridView 事件处理程序 RowUpdating 中,我们需要以编程方式引用 PictureUpload FileUpload 控件,并确定文件是否已上传。 如果未上传一个,则我们 不希望picture 参数指定值。 另一方面,如果在 FileUpload 控件中 PictureUpload 上传了文件,我们希望确保它是 JPG 文件。 如果是,则可以通过 picture 参数将其二进制内容发送到 ObjectDataSource。

与步骤 6 中使用的代码一样,此处所需的大部分代码已存在于 DetailsView 事件处理程序 ItemInserting 中。 因此,我已将通用功能重构为新方法, ValidPictureUpload并更新 ItemInserting 了事件处理程序以使用此方法。

将以下代码添加到 GridView RowUpdating 事件处理程序的开头。 重要的是,此代码位于保存小册子文件的代码之前,因为我们不希望将小册子保存到 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;仅当上传图片文件时才会调用该文件。 如果未上传文件,则不设置图片参数,因此使用其默认值 null。 如果上传并 ValidPictureUpload 返回 true图片,则 picture 为参数分配上传图像的二进制数据;如果该方法返回 false,则更新工作流被取消,事件处理程序退出。

ValidPictureUpload(FileUpload) 方法代码是从 DetailsView 的事件处理程序 ItemInserting 重构出来的,具体如下:

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画图。
  3. 将位图另存为 JPG 图像。
  4. 使用 JPG 文件通过编辑界面更新分类图片。

编辑类别并上传 JPG 图像后,图像将不会在浏览器中呈现,因为 DisplayCategoryPicture.aspx 页面将从前八个类别的图片中去除前 78 个字节。 通过删除执行 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 页面的插入和编辑界面还有改进的空间。 CategoryNameDescription BoundFields 在 DetailsView 和 GridView 中应转换为 TemplateFields。 由于 CategoryName 不允许 NULL 值,因此应添加 RequiredFieldValidator。 Description文本框可能转换为多行 TextBox。 我把这些最后的润色留给你作为练习。

概要

本教程介绍了如何使用二进制数据。 在本教程和前面的三个教程中,我们了解了如何将二进制数据存储在文件系统或直接存储在数据库中。 用户通过从硬盘中选择文件并将其上传到 Web 服务器,将二进制数据存储在文件系统或插入到数据库中,从而向系统提供二进制数据。 ASP.NET 2.0 包括一个 FileUpload 控件,使提供这种界面像使用拖放功能一样简单。 但是,如 上传文件 教程中所述,FileUpload 控件仅适用于相对较小的文件上传,理想情况下不会超过兆字节。 我们还探讨了如何将上传的数据与基础数据模型相关联,以及如何编辑和删除现有记录中的二进制数据。

接下来的一组教程将探讨各种缓存技术。 缓存提供了一种方法,通过获取成本高昂的作的结果并将其存储在可更快地访问的位置来提高应用程序的整体性能。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com