共用方式為


更新及刪除現有的二進位資料 (VB)

作者:Scott Mitchell

下載 PDF

在前面的教學課程中,我們了解了 GridView 控制項如何簡化編輯和刪除文字資料。 在本教學課程中,我們將了解 GridView 控制項如何使編輯和刪除二進位資料成為可能,無論該二進位資料是儲存在資料庫中還是儲存在檔案系統中。

簡介

在過去的三個教學課程中,我們新增了大量用於處理二進位資料的功能。 我們先在 Categories 表中新增 BrochurePath 列,並相應地更新了架構。 我們還新增了資料存取層和商務邏輯層方法來處理類別表的現有 Picture 列,該列儲存了影像檔案的二進位內容。 我們建立了網頁,在 GridView 中呈現二進制資料,並提供手冊的下載連結,類別圖片顯示在 <img> 元素中,並新增了 DetailsView 以允許使用者新增新類別並上傳其手冊和圖片資料。

剩下的工作就是編輯和刪除現有類別的能力,我們將在本教學課程中使用 GridView 的內建編輯和刪除功能來實現這一點。 編輯類別時,使用者可以選擇上傳新圖片或讓類別繼續使用現有圖片。 對於手冊,他們可以選擇使用現有的手冊、上傳新的手冊,或表明該類別不再有與之關聯的手冊。 現在就開始吧!

步驟 1:更新資料存取層

DAL 具有自動產生的 InsertUpdateDelete 方法,但這些方法是基於 CategoriesTableAdapter 的主要查詢產生的,該查詢不包含 Picture 列。 因此,InsertUpdate 方法不包括用於指定類別圖片的二進位資料的參數。 就像我們在先前的教學課程中所做的那樣,我們需要建立一個新的 TableAdapter 方法,用於在指定二進位資料時更新 Categories 表。

開啟類型化資料集,然後從設計器中以滑鼠以滑鼠右鍵按一下 CategoriesTableAdapter 的標題,然後從操作功能表中選擇「新增查詢」以啟動 TableAdapter 查詢設定精靈。 精靈會先詢問我們 TableAdapter 查詢應如何存取資料庫。 選擇使用 SQL 陳述式並按一下「下一步」。 下一步將提示輸入要產生的查詢類型。 由於我們正在建立查詢以向 Categories 表中新增記錄,因此選擇「更新」並按一下「下一步」。

選擇「更新」選項

圖 1:選擇「更新」選項 (點擊查看完整圖片)

我們現在需要指定 UPDATE SQL 陳述式。 此精靈會自動建議一條與 TableAdapter 的主要查詢相對應的 UPDATE 陳述式 (更新 CategoryNameDescriptionBrochurePath 值)。 更改陳述式,使 Picture 列與 @Picture 參數一起包含在內,如下所示:

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

精靈的最後一個畫面要求我們命名新的 TableAdapter 方法。 輸入「UpdateWithPicture」並點擊「完成」。

將新的 TableAdapter 方法命名為 UpdateWithPicture

圖 2:將新的 TableAdapter 方法命名為 UpdateWithPicture (點擊查看完整圖片)

步驟 2:新增商務邏輯層方法

除了更新 DAL 之外,我們還需要更新 BLL 以包含更新和刪除類別的方法。 這些是將從表示層叫用的方法。

要刪除類別,我們可以使用 CategoriesTableAdapter 自動產生的 Delete 方法。 將下列方法新增至 CategoriesBLL班級:

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteCategory(ByVal categoryID As Integer) As Boolean
    Dim rowsAffected As Integer = Adapter.Delete(categoryID)
    ' Return true if precisely one row was deleted, otherwise false
    Return rowsAffected = 1
End Function

在本教學課程中,讓我們建立兩種更新類別的方法,一種需要二進位圖片資料並叫用我們剛剛新增至 CategoriesTableAdapterUpdateWithPicture 方法,另一種僅接受 CategoryNameDescriptionBrochurePath 值,並使用 CategoriesTableAdapter 類別自動產生的 Update 陳述式。 使用兩種方法的基本原理是,在某些情況下,使用者可能希望更新類別的圖片及其其他欄位,在這種情況下,使用者將必須上傳新圖片。 然後可以在 UPDATE 陳述式中使用上傳的圖片的二進位資料。 在其他情況下,使用者可能只對更新名稱和描述感興趣。 但是,如果 UPDATE 陳述式也需要 Picture 列的二進位資料,那麼我們也需要提供該資訊。 這將需要額外存取資料庫以帶回正在編輯的記錄的圖片資料。 因此,我們需要兩種 UPDATE 方法。 商務邏輯層在更新類別時會根據是否提供圖片資料來決定使用哪一種。

為了實現這一點,請在 CategoriesBLL 類別中新增兩個方法,都命名為 UpdateCategory。 第一個應該接受三個 String、一個 Byte 陣列和一個 Integer 作為其輸入參數;第二個,只有三個 String 和一個 IntegerString 輸入參數為類別名稱、描述和手冊檔案路徑,Byte 陣列為類別圖片的二進位內容,以及要更新的 CategoryID 記錄的 Integer 識別。 請注意,如果傳入的 Byte 陣列為 Nothing,則第一個多載將叫用第二個多載:

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, False)> _
Public Function UpdateCategory(categoryName As String, description As String, _
    brochurePath As String, picture() As Byte, categoryID As Integer) As Boolean
    
    ' If no picture is specified, use other overload
    If picture Is Nothing Then
        Return UpdateCategory(categoryName, description, brochurePath, categoryID)
    End If
    ' Update picture, as well
    Dim rowsAffected As Integer = Adapter.UpdateWithPicture _
        (categoryName, description, brochurePath, picture, categoryID)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function
<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateCategory(categoryName As String, description As String, _
    brochurePath As String, categoryID As Integer) As Boolean
    Dim rowsAffected As Integer = Adapter.Update _
        (categoryName, description, brochurePath, categoryID)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

步驟 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 繫結的 ObjectDataSource (CategoriesDataSource)不支援刪除。

若要解決此問題,請按一下 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 中刪除的支援 (點擊查看完整圖片)

花點時間測試一下刪除功能。 Products表的 CategoryIDCategories 表的 CategoryID 之間存在外部索引鍵,因此如果您嘗試刪除前八個類別中的任何一個,您將收到外部索引鍵限制衝突例外狀況。 要測試此功能,請新增一個新類別,並提供手冊和圖片。 我的測試類別 (如圖 6 所示) 包括一個名為 Test.pdf 的測試手冊檔案和一張測試圖片。 圖 7 顯示了新增測試類別後的 GridView。

新增帶有手冊和影像的測試類別

圖 6:新增帶有手冊和影像的測試類別 (點擊查看完整圖片)

插入測試類別後,將顯示在 GridView 中

圖 7:插入測試類別後,它顯示在 GridView 中 (點擊查看完整圖片)

在 Visual Studio 中,重新整理方案總管。 現在您應該在 ~/Brochures 資料夾中看到一個新檔案 Test.pdf (請參閱圖 8)。

接下來,點擊「測試類別」行中的「刪除」連結,導致頁面回傳並觸發 CategoriesBLL 類別的 DeleteCategory 方法。 這將叫用 DAL 的 Delete 方法,從而將適當的 DELETE 陳述式傳送到資料庫。 然後,資料將重新繫結到 GridView,並將標記傳送回用戶端,而測試類別不再存在。

雖然刪除工作流程成功地從 Categories 表中刪除了測試類別記錄,但它並未從 Web 伺服器的檔案系統中刪除其手冊檔案。 重新整理方案總管,您將看到 Test.pdf 仍然位於 ~/Brochures 資料夾中。

Test.pdf 檔案未從 Web 伺服器的檔案系統中刪除

圖 8Test.pdf 檔案未從 Web 伺服器的檔案系統中刪除

步驟 5:移除已刪除類別的手冊檔案

將二進位資料儲存在資料庫外部的缺點之一是,當刪除關聯的資料庫記錄時,必須採取額外的步驟來清理這些檔案。 GridView 和 ObjectDataSource 提供在執行刪除指令之前和之後觸發的事件。 我們實際上需要為操作前和操作後事件建立事件處理常式。 在刪除 Categories 記錄之前,我們需要確定其 PDF 檔案的路徑,但我們不希望在刪除類別之前刪除 PDF,以免出現例外狀況而導致類別未被刪除。

GridView 的 RowDeleting 事件在叫用 ObjectDataSource 的刪除指令之前觸發,而其 RowDeleted事件會在叫用之後觸發。 使用以下程式碼為這兩個事件建立事件處理常式:

' A page variable to "remember" the deleted category's BrochurePath value
Private deletedCategorysPdfPath As String = Nothing
Protected Sub Categories_RowDeleting(sender As Object, e As GridViewDeleteEventArgs) _
    Handles Categories.RowDeleting
    
    ' Determine the PDF path for the category being deleted...
    Dim categoryID As Integer = Convert.ToInt32(e.Keys("CategoryID"))
    Dim categoryAPI As New CategoriesBLL()
    Dim categoriesData As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = categoriesData(0)
    If category.IsBrochurePathNull() Then
        deletedCategorysPdfPath = Nothing
    Else
        deletedCategorysPdfPath = category.BrochurePath
    End If
End Sub
Protected Sub Categories_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
    Handles Categories.RowDeleted
    
    ' Delete the brochure file if there were no problems deleting the record
    If e.Exception Is Nothing Then
        DeleteRememberedBrochurePath()
    End If
End Sub

RowDeleting 事件處理常式中,從 GridView 的 DataKeys 集合中取得要刪除行的 CategoryID,可以在該事件處理常式中透過 e.Keys 集合存取該集合。 接下來,叫用 CategoriesBLL 類別的 GetCategoryByCategoryID(categoryID),傳回有關被刪除記錄的資訊。 如果傳回的 CategoriesDataRow 物件具有非 NULL``BrochurePath 的值,則將其儲存在頁面變數 deletedCategorysPdfPath 中,以便可以在 RowDeleted 事件處理常式中刪除該檔案。

注意

我們可以將 BrochurePath 新增至 GridView 的 DataKeyNames 屬性並透過 e.Keys 集合存取記錄的值,而不是檢索在 RowDeleting 事件處理常式中刪除的 Categories 記錄的 BrochurePath 詳細資訊。 這樣做會稍微增加 GridView 的檢視狀態大小,但會減少所需的程式碼量並節省資料庫存取次數。

在叫用 ObjectDataSource 的底層刪除指令後,會觸發 GridView 的 RowDeleted 事件處理常式。 如果刪除資料時沒有出現例外狀況且存在 deletedCategorysPdfPath 值,則 PDF 將從檔案系統中刪除。 請注意,不需要此額外程式碼來清理與其圖片關聯的類別的二進位資料。 這是因為圖片資料是直接儲存在資料庫中的,所以刪除 Categories 行也會刪除該類別的圖片資料。

新增兩個事件處理常式後,再次執行此測試案例。 刪除類別時,其關聯的 PDF 也會被刪除。

更新現有記錄的關聯二進位資料帶來了一些有趣的挑戰。 本教學課程的其餘部分將深入研究為手冊和圖片新增更新功能。 步驟 6 探討更新手冊資訊的技術,而第 7 步則研究更新圖片。

第 6 步:更新類別手冊

正如「插入、更新和刪除資料概述」教學課程中所討論的,GridView 提供內建的行級編輯支援,如果其基礎資料來源設定正確,則可以透過勾選核取方塊來實現。 目前,CategoriesDataSource ObjectDataSource 尚未設定為包括更新支援,因此讓我們新增它。

按一下 ObjectDataSource 精靈中的「設定資料來源」連結,然後繼續步驟 2驟。 由於 CategoriesBLL 中使用了 DataObjectMethodAttribute,因此 UPDATE 下拉式清單應自動填入接受四個輸入參數的 UpdateCategory 多載 (對於除 Picture 之外的所有列)。 更改此設定,使其使用具有五個參數的多載。

設定 ObjectDataSource 以使用包含 Picture 參數的 UpdateCategory 方法

圖 9:設定 ObjectDataSource 以使用包含 Picture 參數的 UpdateCategory 方法 (點擊查看完整圖片)

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 BoundFields 會轉譯為文字輸入框。 BrochurePath TemplateField 缺少 EditItemTemplate,因此它繼續顯示其 ItemTemplate 到手冊的連結。 Picture ImageField 會轉譯為 TextBox,其 Text 屬性被指定為 ImageField 的 DataImageUrlField 值,在本例中為 CategoryID

GridView 缺少 BrochurePath 的編輯介面

圖 11:GridView 缺少 BrochurePath 的編輯介面 (點擊查看完整圖片)

自訂 BrochurePath 的編輯介面

我們需要為 BrochurePath TemplateField 建立一個編輯介面,允許使用者執行以下任一操作:

  • 保留類別手冊不變,
  • 透過上傳新的手冊來更新類別的手冊,或者
  • 完全刪除類別的手冊 (如果該類別不再有關聯的手冊)。

我們還需要更新 Picture ImageField 的編輯介面,但我們將在步驟 7 中完成此操作。

從 GridView 的智慧標籤中,按一下「編輯範本」連結,然後從下拉式清單中選擇 BrochurePath TemplateField 的 EditItemTemplate。 將 RadioButtonList Web 控制項新增至此範本,將其 ID 屬性設為BrochureOptions,並將其 AutoPostBack 屬性設為 True。 在「屬性」視窗中,按一下 Items 屬性中的省略號,這將開啟 ListItem集合編輯器。 新增以下三個選項,分別為 Value 1、2 和 3:

  • 使用目前的手冊
  • 刪除目前的手冊
  • 上傳新手冊

將第一個 ListItemSelected 屬性設為 True

將三個 ListItem 新增至 RadioButtonList

圖 12:將三個 ListItem 新增至 RadioButtonList

在 RadioButtonList 下方,新增一個名為 BrochureUpload 的 FileUpload 控制項。 將其 Visible 屬性設為 False

將 RadioButtonList 和 FileUpload 控制項新增至 EditItemTemplate

圖 13:將 RadioButtonList 和 FileUpload 控制項新增至 EditItemTemplate (點擊查看完整圖片)

此 RadioButtonList 為使用者提供了三個選項。 這個想法是,只有在選擇了最後一個選項「上傳新手冊」時,才會顯示 FileUpload 控制項。 為此,請為 RadioButtonList 的 SelectedIndexChanged 事件建立事件處理常式並新增以下程式碼:

Protected Sub BrochureOptions_SelectedIndexChanged _
    (sender As Object, e As EventArgs)
    
    ' Get a reference to the RadioButtonList and its Parent
    Dim BrochureOptions As RadioButtonList = _
        CType(sender, RadioButtonList)
    Dim parent As Control = BrochureOptions.Parent
    ' Now use FindControl("controlID") to get a reference of the 
    ' FileUpload control
    Dim BrochureUpload As FileUpload = _
        CType(parent.FindControl("BrochureUpload"), FileUpload)
    ' Only show BrochureUpload if SelectedValue = "3"
    BrochureUpload.Visible = (BrochureOptions.SelectedValue = "3")
End Sub

由於 RadioButtonList 和 FileUpload 控制項位於範本內,因此我們必須編寫一些程式碼來以程式設計方式存取這些控制項。 SelectedIndexChanged 事件處理常式將在 sender 輸入參數中傳遞 RadioButtonList 的參考。 要取得 FileUpload 控制項,我們需要取得 RadioButtonList 的父控制項並使用其中的 FindControl("controlID") 方法。 一旦我們參考了 RadioButtonList 和 FileUpload 控制項,僅當 RadioButtonList 的 SelectedValue 等於 3 (這是上傳新手冊 ListItemValue) 時,FileUpload 控制項的 Visible 屬性才會設為 True

準備好此程式碼後,請花點時間測試編輯介面。 點選行的編輯按鈕。 最初,應選擇「使用目前手冊」選項。 更改所選索引會導致回傳。 如果選擇第三個選項,則顯示 FileUpload 控制項,否則隱藏。 圖14 為首次點選「編輯」按鈕時的編輯介面;圖 15 顯示了選擇「上傳新手冊」選項後的介面。

一開始,選擇了「使用目前手冊」選項。

圖 14:一開始,選擇了「使用目前手冊」選項 (點擊查看完整圖片)

選擇「上傳新手冊」選項會顯示 FileUpload 控制項

圖 15:選擇「上傳新手冊」選項會顯示 FileUpload 控制項 (點擊查看完整圖片)

儲存手冊檔案並更新 BrochurePath

當點擊 GridView 的「更新」按鈕時,會觸發其 RowUpdating 事件。 叫用 ObjectDataSource 的更新指令,然後觸發 GridView 的 RowUpdated 事件。 與刪除工作流程一樣,我們需要為這兩個事件建立事件處理常式。 在 RowUpdating 事件處理常式中,我們需要根據 BrochureOptions RadioButtonList 的 SelectedValue 來決定要執行的動作:

  • 如果 SelectedValue 為 1,我們希望繼續使用相同的 BrochurePath 設定。 因此,我們需要將 ObjectDataSource s 的 brochurePath 參數設定為正在更新的記錄的現有 BrochurePath 值。 可以使用 e.NewValues["brochurePath"] = value 設定 ObjectDataSource 的 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
  • 如果 deletedCategorysPdfPath 不是 null,則 DeleteRememberedBrochurePath 刪除頁面變數 deletedCategorysPdfPath 中的路徑指定的檔案。

這兩種方法的程式碼如下。 請注意 ProcessBrochureUpload 與先前教學課程中 DetailsView 的 ItemInserting 事件處理常式之間的相似性。 在本教學課程中,我更新了 DetailsView 事件處理常式以使用這些新方法。 下載與本教學課程相關的程式碼以查看 DetailsView 事件處理常式的修改。

Private Function ProcessBrochureUpload _
    (BrochureUpload As FileUpload, CancelOperation As Boolean) As String
    
    CancelOperation = False    ' by default, do not cancel operation
    If BrochureUpload.HasFile Then
        ' Make sure that a PDF has been uploaded
        If String.Compare(System.IO.Path.GetExtension(BrochureUpload.FileName), _
            ".pdf", True) <> 0 Then
            
            UploadWarning.Text = _
                "Only PDF documents may be used for a category's brochure."
            UploadWarning.Visible = True
            CancelOperation = True
            Return Nothing
        End If
        Const BrochureDirectory As String = "~/Brochures/"
        Dim brochurePath As String = BrochureDirectory + BrochureUpload.FileName
        Dim fileNameWithoutExtension As String = _
            System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName)
        Dim iteration As Integer = 1
        While System.IO.File.Exists(Server.MapPath(brochurePath))
            brochurePath = String.Concat(BrochureDirectory, _
                fileNameWithoutExtension, "-", iteration, ".pdf")
            iteration += 1
        End While
        ' 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 Nothing
    End If
End Function
Private Sub DeleteRememberedBrochurePath()
    ' Is there a file to delete?
    If deletedCategorysPdfPath IsNot Nothing Then
        System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath))
    End If
End Sub

GridView 的 RowUpdatingRowUpdated 事件處理常式使用 ProcessBrochureUploadDeleteRememberedBrochurePath 方法,如以下程式碼所示:

Protected Sub Categories_RowUpdating _
    (sender As Object, e As GridViewUpdateEventArgs) _
    Handles Categories.RowUpdating
    
    ' Reference the RadioButtonList
    Dim BrochureOptions As RadioButtonList = _
        CType(Categories.Rows(e.RowIndex).FindControl("BrochureOptions"), _
            RadioButtonList)
    ' Get BrochurePath information about the record being updated
    Dim categoryID As Integer = Convert.ToInt32(e.Keys("CategoryID"))
    Dim categoryAPI As New CategoriesBLL()
    Dim categoriesData As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = categoriesData(0)
    If BrochureOptions.SelectedValue = "1" Then
        ' Use current value for BrochurePath
        If category.IsBrochurePathNull() Then
            e.NewValues("brochurePath") = Nothing
        Else
            e.NewValues("brochurePath") = category.BrochurePath
        End If
    ElseIf BrochureOptions.SelectedValue = "2" Then
        ' Remove the current brochure (set it to NULL in the database)
        e.NewValues("brochurePath") = Nothing
    ElseIf BrochureOptions.SelectedValue = "3" Then
        ' Reference the BrochurePath FileUpload control
        Dim BrochureUpload As FileUpload = _
            CType(categories.Rows(e.RowIndex).FindControl("BrochureUpload"), _
                FileUpload)
        ' Process the BrochureUpload
        Dim cancelOperation As Boolean = False
        e.NewValues("brochurePath") = _
            ProcessBrochureUpload(BrochureUpload, cancelOperation)
        e.Cancel = cancelOperation
    Else
        ' Unknown value!
        Throw New ApplicationException( _
            String.Format("Invalid BrochureOptions value, {0}", _
                BrochureOptions.SelectedValue))
    End If
    If BrochureOptions.SelectedValue = "2" OrElse _
        BrochureOptions.SelectedValue = "3" Then
        
        ' "Remember" that we need to delete the old PDF file
        If (category.IsBrochurePathNull()) Then
            deletedCategorysPdfPath = Nothing
        Else
            deletedCategorysPdfPath = category.BrochurePath
        End If
    End If
End Sub
Protected Sub Categories_RowUpdated _
    (sender As Object, e As GridViewUpdatedEventArgs) _
    Handles Categories.RowUpdated
    
    ' If there were no problems and we updated the PDF file, 
    ' then delete the existing one
    If e.Exception Is Nothing Then
        DeleteRememberedBrochurePath()
    End If
End Sub

請注意 RowUpdating 事件處理常式如何使用一系列條件式陳述式,根據 BrochureOptions RadioButtonList 的 SelectedValue 屬性值執行適當的操作。

有了此程式碼,您可以編輯一個類別並讓它使用目前的手冊、不使用手冊或上傳新的手冊。 來試試看。在 RowUpdatingRowUpdated 事件處理常式中設定斷點以了解工作流程。

步驟 7:上傳新圖片

Picture ImageField 的編輯介面呈現為一個文字輸入框,其中填充了其 DataImageUrlField 屬性中的值。 在編輯工作流程期間,GridView 會向 ObjectDataSource 傳遞一個參數,其中參數名稱是 ImageField DataImageUrlField 屬性的值,參數值是在編輯介面中的文字輸入框中輸入的值。 當影像在檔案系統上另存為檔案,並且 DataImageUrlField 包含影像的完整 URL 時,此行為適用。 在這種情況下,編輯介面會在文字輸入框中顯示影像的 URL,使用者可以變更該 URL 並將其儲存回資料庫。 當然,這個預設介面不允許使用者上傳新影像,但它允許他們將影像的 URL 從目前值更改為另一個值。 然而,對於本教學課程來說,ImageField 的預設編輯介面還不夠,因為 Picture 二進位資料直接儲存在資料庫中,並且 DataImageUrlField 屬性僅儲存 CategoryID

為了更好地理解在我們的教學課程中,當使用者編輯帶有 ImageField 的行時會發生什麼,請考慮以下範例:使用者編輯帶有 CategoryID 10 的行,導致 Picture ImageField 呈現為值為 10 的文字輸入框。 想像一下,使用者將此文字輸入框中的值更改為 50,並點擊「更新」按鈕。 發生回傳,GridView 最初會建立一個名為 CategoryID 且值為 50 的參數。 但是,在 GridView 傳送此參數 (以及 CategoryNameDescription 參數) 之前,它會新增 DataKeys 集合中的值。 因此,它用目前行的基礎 CategoryID 值 10 覆蓋 CategoryID 參數。 簡而言之,ImageField 的 DataImageUrlField 編輯介面對本教學課程的編輯工作流程沒有影響,因為 ImageField 的屬性名稱和 grid 的 DataKey 值是相同的。

雖然 ImageField 可以輕鬆顯示基於資料庫資料的影像,但我們不想在編輯介面中提供文字輸入框。 相反,我們希望提供一個 FileUpload 控制項,終端使用者可以使用該控制項來更改類別的圖片。 與 BrochurePath 值不同,對於這些教學課程,我們決定要求每個類別都必須有圖片。 因此,我們不需要讓使用者表明沒有關聯的圖片,使用者可以上傳新圖片或保留目前圖片不變。

為了自訂 ImageField 的編輯介面,我們需要將其轉換為 TemplateField。 從 GridView 的智慧標籤中,點擊「編輯列」連結,選擇「ImageField」,然後點擊「將此欄位轉換為 TemplateField」連結。

將 ImageField 轉換為 TemplateField

圖 16:將 ImageField 轉換為 TemplateField

以這種方式將 ImageField 轉換為 TemplateField 會產生具有兩個範本的 TemplateField。 如以下宣告式語法所示,ItemTemplate 包含一個 Image Web 控制項,其 ImageUrl 屬性是使用基於 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 的 EditItemTemplate。 在範本中,您應該會看到一個文字輸入框將其刪除。 接下來,將 FileUpload 控制項從工具箱拖曳到範本中,並將其 ID 設為 PictureUpload。 同時新增文字「若要更改類別的圖片,請指定新圖片」。 若要保持類別圖片相同,請將範本的欄位留空。

將 FileUpload 控制項新增至 EditItemTemplate

圖 17:將 FileUpload 控制項新增至 EditItemTemplate (點擊查看完整圖片)

自訂編輯介面後,可以在瀏覽器中查看進度。 在唯讀模式下檢視行時,類別的影像會像先前一樣顯示,但按一下「編輯」按鈕會將圖片列轉譯為具有 FileUpload 控制項的文字。

編輯介面包含 FileUpload 控制項

圖 18:編輯介面包含 FileUpload 控制項 (點擊查看完整圖片)

回想一下,ObjectDataSource 設定為呼叫 CategoriesBLL 類別的 UpdateCategory 方法,該方法會接受圖片的二進位資料作為 Byte 陣列的輸入。 但是,如果該陣列有 Nothing,則會呼叫備用 UpdateCategory 多載,該多載會發出不修改 Picture 列的 UPDATE SQL 陳述式,從而使類別的目前圖片保持不變。 因此,在 GridView 的 RowUpdating 事件處理常式中,我們需要以程式設計方式參考 PictureUpload FileUpload 控制項,並確定檔案是否已上傳。 如果未上傳,那麼我們想為 picture 參數指定值。 另一方面,如果在 PictureUpload FileUpload 控制項中上傳檔案,我們要確保它是 JPG 檔案。 如果是,那麼我們可以透過 picture 參數將其二進位內容傳送到 ObjectDataSource。

與步驟 6 中使用的程式碼一樣,此處所需的大部分程式碼已存在於 DetailsView 的 ItemInserting 事件處理常式中。 因此,我將常用功能重構為新方法 ValidPictureUpload,並更新了 ItemInserting 事件處理常式以使用此方法。

將以下程式碼新增至 GridView 的 RowUpdating 事件處理常式的開頭。 重要的是,此程式碼位於儲存手冊文件的程式碼之前,因為如果上傳了無效的圖片文件,我們不想將手冊儲存到 Web 伺服器的檔案系統中。

' Reference the PictureUpload FileUpload
Dim PictureUpload As FileUpload = _
    CType(categories.Rows(e.RowIndex).FindControl("PictureUpload"), _
        FileUpload)
If PictureUpload.HasFile Then
    ' Make sure the picture upload is valid
    If ValidPictureUpload(PictureUpload) Then
        e.NewValues("picture") = PictureUpload.FileBytes
    Else
        ' Invalid file upload, cancel update and exit event handler
        e.Cancel = True
        Exit Sub
    End If
End If

ValidPictureUpload(FileUpload) 方法以FileUpload控制項作為其唯一的輸入參數,並檢查上傳檔案的副檔名以確保上傳的檔案是 JPG;只有當上傳圖片檔案時才會呼叫它。 如果沒有上傳文件,則不會設定圖片參數,因此會使用其 Nothing 的預設值。 如果上傳了圖片並且 ValidPictureUpload 傳回 True,則 picture 參數分配為上傳圖片的二進位資料;如果該方法傳回 False,則更新工作流程將會取消,並結束事件處理常式。

ValidPictureUpload(FileUpload) 方法程式碼是從 DetailsView 的 ItemInserting 事件處理常式重構的,如下所示:

Private Function ValidPictureUpload(ByVal PictureUpload As FileUpload) As Boolean
    ' Make sure that a JPG has been uploaded
    If String.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), _
        ".jpg", True) <> 0 AndAlso _
        String.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), _
        ".jpeg", True) <> 0 Then
        
        UploadWarning.Text = _
            "Only JPG documents may be used for a category's picture."
        UploadWarning.Visible = True
        Return False
    Else
        Return True
    End If
End Function

步驟 8:用 JPG 取代原始類別圖片

回想一下,原始的八個類別圖片是封裝在 OLE 標頭中的點陣圖檔。 現在我們已經新增了編輯現有記錄圖片的功能,請花點時間用 JPG 取代這些點陣圖。 如果您想繼續使用目前類別圖片,可以透過以下步驟將其轉換為JPG:

  1. 將點陣圖影像儲存到硬碟上。 在瀏覽器中造訪 UpdatingAndDeleting.aspx 頁面,對於前八個類別中的每個類別,以滑鼠以滑鼠右鍵按一下影像並選擇儲存圖片。
  2. 在您選擇的影像編輯器中開啟影像。 例如,您可以使用 Microsoft Paint。
  3. 將點陣圖儲存為 JPG 影像。
  4. 透過編輯介面更新類別圖片,使用JPG檔。

編輯類別並上傳 JPG 影像後,該影像將不會在瀏覽器中轉譯,因為 DisplayCategoryPicture.aspx 頁面會從前 8 個類別的圖片中剝離前 78 位元組。 透過刪除執行 OLE 標頭剝離的程式碼來修復此問題。 完成此操作後,DisplayCategoryPicture.aspx``Page_Load 事件處理常式應該只有以下程式碼:

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    Dim categoryID As Integer = _
        Convert.ToInt32(Request.QueryString("CategoryID"))
    ' Get information about the specified category
    Dim categoryAPI As New CategoriesBLL()
    Dim categories As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryWithBinaryDataByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = 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)
End Sub

注意

UpdatingAndDeleting.aspx 頁面的插入和編輯介面可能需要更多的工作。 DetailsView 和 GridView 中的 CategoryNameDescription BoundFields 應轉換為 TemplateFields。 由於 CategoryName 不允許使用 NULL 值,因此應新增 RequiredFieldValidator。 並且 Description TextBox 應該轉換為多行 TextBox。 我把這些最後的潤飾留給您當作練習。

摘要

本教學課程完成了我們對二進位資料處理的了解。 在本教學課程和前三個教學課程中,我們了解如何將二進位資料儲存在檔案系統上或直接儲存在資料庫中。 使用者透過從硬碟中選擇檔案並將其上傳到 Web 伺服器來向系統提供二進位資料,在 Web 伺服器上可以將其儲存在檔案系統上或插入到資料庫中。 ASP.NET 2.0 包含一個 FileUpload 控制項,讓提供此類介面就像拖放一樣簡單。 但是,如「上傳檔案」教學課程中所述,FileUpload 控制項僅適合上傳相對較小的文件,最好不超過 1 MB。 我們也探討如何將上傳的資料與底層資料模型相關聯,以及如何從現有記錄中編輯和刪除二進位資料。

我們的下一組教學課程將探討各種快取技術。 快取提供了一種改善應用程式整體效能的方法,透過將耗時操作的結果儲存到可以更快存取的位置。

祝您程式設計愉快!

關於作者

Scott Mitchell,七本 ASP/ASP.NET 書籍的作者和 4GuysFromRolla.com 創始人,自 1998 年以來便開始使用 Microsoft Web 技術。 Scott 擔任獨立顧問、講師和作家。 他的新書是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以透過 mitchell@4GuysFromRolla.com 或他的部落格 (可以在 http://ScottOnWriting.NET 找到) 與他聯繫。

特別感謝

本教學課程系列已經過許多熱心的檢閱者檢閱。 本教學課程的主要審閱者是 Teresa Murphy。 有興趣檢閱我即將推出的 MSDN 文章嗎? 如果有的話請寫信給我,信箱是 mitchell@4GuysFromRolla.com。