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

作者 :Scott Mitchell

下载 PDF

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

简介

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

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

步骤 1:更新数据访问层

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

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

选择“更新”选项

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

我们现在需要指定 UPDATE SQL 语句。 该向导会自动建议一个UPDATE对应于 TableAdapter main 查询的语句, (更新 CategoryNameDescription) 、 和 BrochurePath 值。 更改 语句,以便将 Picture 列与参数一 @Picture 起包含,如下所示:

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

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

将 New TableAdapter 方法命名为 UpdateWithPicture

图 2:将 New 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;
}

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

为了方便执行此操作,请将两个方法添加到 类, CategoriesBLL 两个方法都名为 UpdateCategory。 第一个应接受三 string 个 、一个 byte 数组和 一个 int 作为其输入参数;第二个应接受三 string 个 s 和一个 int。 输入string参数用于类别的名称、说明和小册子文件路径,byte数组用于类别图片的二进制内容,标识intCategoryID要更新的记录的 。 请注意,如果传入 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

从 UploadInDetailsView.aspx 复制声明性标记

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

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

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

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

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

现在,请将“更新”选项卡下拉列表设置为“ (无”) ,但将“删除”选项卡下拉列表设置为 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。 这会将 CommandField 添加到 GridView,其 ShowDeleteButton 属性设置为 true

在 GridView 中启用对删除的支持

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

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

使用宣传册和图像添加测试类别

图 6:使用宣传册和图像添加测试类别 (单击以查看全尺寸图像)

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

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

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

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

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

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

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

步骤 5:删除已删除类别的宣传册文件

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

GridView 事件RowDeleting在调用 ObjectDataSource 的 delete 命令之前触发,而其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));
        }
    }
}

在事件处理程序中 RowDeletingCategoryID 从 GridView 的 集合中抓取要删除的行的 DataKeys ,可以通过 集合在此事件处理程序中访问该 e.Keys 集合。 接下来, CategoriesBLL 调用 类 s GetCategoryByCategoryID(categoryID) 以返回有关要删除的记录的信息。 如果返回 CategoriesDataRow 的对象具有非NULL``BrochurePath 值,则它存储在页变量 deletedCategorysPdfPath 中,以便可以在事件处理程序中删除 RowDeleted 该文件。

注意

或者,我们可以将 添加到 GridView 的 DataKeyNames 属性,并通过 集合访问记录值e.Keys,而不是检索BrochurePath事件处理程序中RowDeleting删除的记录的详细信息CategoriesBrochurePath 这样做会略微增加 GridView 的视图状态大小,但会减少所需的代码量,并保存数据库的行程。

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

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

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

步骤 6:更新类别手册

插入、更新和删除数据概述 教程中所述,GridView 提供内置的行级编辑支持,如果正确配置了其基础数据源,则可以通过复选框的刻度实现。 目前, CategoriesDataSource ObjectDataSource 尚未配置为包含更新支持,因此让我们将其添加到 中。

单击 ObjectDataSource 向导中的“配置数据源”链接,然后继续执行第二个步骤。 由于 在 中使用CategoriesBLLDataObjectMethodAttribute ,因此 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 的 ShowEditButton 属性设置为 true,从而为) 编辑的行添加“编辑”按钮 (和“更新”和“取消”按钮。

配置 GridView 以支持编辑

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

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

GridView 缺少宣传册路径的编辑界面

图 11:GridView 缺少用于 BrochurePath (单击以查看全尺寸图像)

自定义BrochurePath编辑界面

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

  • 将类别的宣传册保留为原样,
  • 通过上传新的宣传册来更新类别的宣传册,或者
  • 如果类别不再具有关联的小册子) ,请完全 (删除类别的宣传册。

我们还需要更新 Picture ImageField 的编辑界面,但在步骤 7 中将对此进行介绍。

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

  • 使用当前手册
  • 删除当前宣传册
  • 上传新宣传册

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

将三个 ListItems 添加到 RadioButtonList

图 12:向 RadioButtonList 添加 3 ListItem

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

将 RadioButtonList 和 FileUpload 控件添加到 EditItemTemplate

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

此 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 在输入参数中传递 RadioButtonList 的 sender 引用。 若要获取 FileUpload 控件,我们需要获取 RadioButtonList 的父控件,并从那里使用 FindControl("controlID") 方法。 同时引用 RadioButtonList 和 FileUpload 控件后,仅当 RadioButtonList 等于 Visible 3 时,FileUpload 控件的 SelectedValue 属性才会设置为 true ,这是Value上传新手册ListItem的 。

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

最初,“使用当前手册”选项处于选中状态

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

选择“上传新宣传册”选项显示“文件”“上传”控件

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

保存宣传册文件并更新BrochurePath

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

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

当 RadioButtonList 为 SelectedValue 3 时需要完成的步骤与 DetailsView 事件处理程序 ItemInserting 所用的步骤几乎相同。 从 上一教程中添加的 DetailsView 控件添加新的类别记录时,将执行此事件处理程序。 因此,需要将此功能重构为单独的方法。 具体而言,我将常用功能移到了两种方法中:

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

下面介绍了这两种方法的代码。 请注意与上一教程中的 DetailsView 事件处理程序ItemInserting之间的ProcessBrochureUpload相似性。 在本教程中,我更新了 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 属性值执行适当的操作。

使用此代码后,可以编辑类别并使其使用其当前宣传册、不使用宣传册或上传新宣传册。 继续尝试一下。在 和 RowUpdated 事件处理程序中RowUpdating设置断点,了解工作流。

步骤 7:上传新图片

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

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

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

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

将 ImageField 转换为 TemplateField

图 16:将 ImageField 转换为 TemplateField

以这种方式将 ImageField 转换为 TemplateField 会生成包含两个模板的 TemplateField。 如以下声明性语法所示, ItemTemplate 包含一个图像 Web 控件,该控件使用基于 ImageField 和 DataImageUrlFieldDataImageUrlFormatString 属性的数据绑定语法分配其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 智能标记中,单击“编辑模板”链接,然后从下拉列表中选择 Picture “TemplateField EditItemTemplate ”。 在模板中,应会看到一个 TextBox 删除此内容。 接下来,将 FileUpload 控件从工具箱拖动到模板中,并将其 ID 设置为 PictureUpload。 另请添加文本 若要更改类别图片,请指定新图片。 若要使类别的图片保持不变,请将字段留空给模板。

将 FileUpload 控件添加到 EditItemTemplate

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

自定义编辑界面后,在浏览器中查看进度。 在只读模式下查看某一行时,类别的图像将像以前一样显示,但单击“编辑”按钮会使用 FileUpload 控件将图片列呈现为文本。

编辑接口包括 FileUpload 控件

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

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

与步骤 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返回 ,则picture为 参数分配上传图像的二进制数据;如果 方法返回 false,则取消更新工作流true并退出事件处理程序。

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页面的插入和编辑界面可能会使用更多的工作。 CategoryName DetailsView 和 GridView 中的 和 Description BoundFields 应转换为 TemplateFields。 由于 CategoryName 不允许 NULL 值,因此应添加 RequiredFieldValidator。 Description TextBox 可能应转换为多行 TextBox。 我把这些最后的润色作为练习给你。

总结

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

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

编程快乐!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。

特别感谢

本教程系列由许多有用的审阅者查看。 本教程的首席审阅者是 Teresa Murphy。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。