Bagikan melalui


Menerapkan Konkurensi Optimis (C#)

oleh Scott Mitchell

Unduh PDF

Untuk aplikasi web yang memungkinkan beberapa pengguna mengedit data, ada risiko bahwa dua pengguna mungkin mengedit data yang sama secara bersamaan. Dalam tutorial ini kita akan menerapkan kontrol konkurensi optimis untuk menangani risiko ini.

Pendahuluan

Untuk aplikasi web yang hanya memungkinkan pengguna melihat data, atau aplikasi dengan satu pengguna yang dapat memodifikasi data, tidak ada risiko dua pengguna bersamaan secara tidak sengaja menimpa perubahan satu sama lain. Namun, untuk aplikasi web yang memungkinkan beberapa pengguna memperbarui atau menghapus data, ada potensi modifikasi satu pengguna untuk berbenturan dengan pengguna bersamaan lainnya. Tanpa kebijakan konkurensi apa pun, ketika dua pengguna secara bersamaan mengedit satu rekaman, pengguna yang melakukan perubahannya terakhir akan mengambil alih perubahan yang dilakukan oleh yang pertama.

Misalnya, bayangkan bahwa dua pengguna, Jisun dan Sam, keduanya mengunjungi halaman di aplikasi kami yang memungkinkan pengunjung untuk memperbarui dan menghapus produk melalui kontrol GridView. Keduanya klik tombol Edit di GridView di sekitar waktu yang sama. Jisun mengubah nama produk menjadi "Chai Tea" dan mengklik tombol Perbarui. Hasil bersih adalah UPDATE pernyataan yang dikirim ke database, yang mengatur semua bidang produk yang dapat diperbarui (meskipun Jisun hanya memperbarui satu bidang, ProductName). Pada titik waktu ini, database memiliki nilai "Chai Tea," kategori Minuman, pemasok Exotic Liquids, dan sebagainya untuk produk khusus ini. Namun, GridView di layar Sam masih menampilkan nama produk di baris GridView yang dapat diedit sebagai "Chai". Beberapa detik setelah perubahan Jisun dilakukan, Sam memperbarui kategori menjadi Bumbu dan mengklik Perbarui. Ini menghasilkan pernyataan UPDATE yang dikirim ke database yang mengatur nama produk "Chai," CategoryID ke ID kategori Minuman yang sesuai, dan sebagainya. Perubahan Jisun pada nama produk telah digantikan. Gambar 1 secara grafis menggambarkan rangkaian peristiwa ini.

Ketika Dua Pengguna Secara Bersamaan Memperbarui Catatan, Ada Potensi Perubahan dari Satu Pengguna Menimpa Perubahan Pengguna Lain

Gambar 1: Ketika Dua Pengguna Secara Bersamaan Memperbarui Catatan, Ada Kemungkinan Perubahan yang Dilakukan oleh Satu Pengguna Menimpa Perubahan Pengguna Lain (Klik untuk melihat gambar ukuran penuh)

Demikian pula, ketika dua pengguna mengunjungi halaman, satu pengguna mungkin berada di tengah-tengah memperbarui rekaman saat dihapus oleh pengguna lain. Atau, antara saat pengguna memuat halaman dan ketika mereka mengklik tombol Hapus, pengguna lain mungkin telah memodifikasi konten rekaman tersebut.

Ada tiga strategi kontrol konkurensi yang tersedia:

  • Jangan Lakukan Apa-apa -if pengguna yang bersamaan memodifikasi rekaman yang sama, biarkan komitmen terakhir menang (perilaku default)
  • Konkurensi Optimis - asumsikan bahwa meskipun mungkin ada konflik konkurensi setiap saat dan kemudian, sebagian besar waktu konflik tersebut tidak akan muncul; oleh karena itu, jika konflik muncul, cukup beri tahu pengguna bahwa perubahan mereka tidak dapat disimpan karena pengguna lain telah memodifikasi data yang sama
  • Konkurensi Pesimis - mengasumsikan bahwa konflik konkurensi adalah hal yang biasa dan pengguna tidak akan mentolerir bahwa perubahan mereka tidak disimpan karena aktivitas bersamaan pengguna lain; oleh karena itu, ketika satu pengguna mulai memperbarui rekaman, menguncinya, sehingga mencegah pengguna lain mengedit atau menghapus rekaman itu sampai pengguna melakukan modifikasi mereka

Semua tutorial kami sejauh ini telah menggunakan strategi penyelesaian concurrency default - yaitu, kami membiarkan hasil penulisan terakhir yang menang. Dalam tutorial ini kita akan memeriksa cara menerapkan kontrol konkurensi optimis.

Nota

Kita tidak akan melihat contoh konkurensi pesimis dalam seri tutorial ini. Konkurensi pesimis jarang digunakan karena kunci tersebut, jika tidak dilepaskan dengan benar, dapat mencegah pengguna lain memperbarui data. Misalnya, jika pengguna mengunci rekaman untuk pengeditan dan kemudian pergi untuk hari sebelum membuka kuncinya, tidak ada pengguna lain yang akan dapat memperbarui rekaman itu sampai pengguna asli kembali dan menyelesaikan pembaruannya. Oleh karena itu, dalam situasi di mana konkurensi pesimistik digunakan, biasanya ada jeda waktu yang, jika tercapai, akan membatalkan kunci. Situs web penjualan tiket, yang mengunci lokasi tempat duduk tertentu untuk waktu yang singkat sementara pengguna menyelesaikan proses pesanan, adalah contoh kontrol konkurensi pesimis.

Langkah 1: Melihat Bagaimana Konkurensi Optimis Diterapkan

Kontrol konkurensi optimis bekerja dengan memastikan bahwa rekaman yang diperbarui atau dihapus memiliki nilai yang sama seperti saat proses pembaruan atau penghapusan dimulai. Misalnya, saat mengklik tombol Edit di GridView yang dapat diedit, nilai rekaman dibaca dari database dan ditampilkan di Kotak Teks dan kontrol Web lainnya. Nilai asli ini disimpan oleh GridView. Kemudian, setelah pengguna membuat perubahannya dan mengklik tombol Perbarui, nilai asli ditambah nilai baru dikirim ke Lapisan Logika Bisnis, lalu turun ke Lapisan Akses Data. Lapisan Akses Data harus mengeluarkan pernyataan SQL yang hanya akan memperbarui rekaman jika nilai asli yang mulai diedit pengguna identik dengan nilai yang masih ada dalam database. Gambar 2 menggambarkan urutan peristiwa ini.

Agar Pembaruan atau Penghapusan Berhasil, Nilai Asli Harus Sama dengan Nilai Database Saat Ini

Gambar 2: Agar Pembaruan atau Penghapusan Berhasil, Nilai Asli Harus Sama dengan Nilai Database Saat Ini (Klik untuk melihat gambar ukuran penuh)

Ada berbagai pendekatan untuk menerapkan konkurensi optimis (lihat Logika Pembaruan Konkurensi OptimisPeter A. Bromberg untuk melihat beberapa opsi). ADO.NET Typed DataSet menyediakan satu implementasi yang dapat dikonfigurasi hanya dengan centang kotak centang. Mengaktifkan concurensi optimis untuk TableAdapter dalam Typed DataSet menambah pernyataan UPDATE dan DELETE pada TableAdapter untuk menyertakan perbandingan semua nilai asli dalam klausul WHERE. Pernyataan berikut UPDATE , misalnya, memperbarui nama dan harga produk hanya jika nilai database saat ini sama dengan nilai yang awalnya diambil saat memperbarui rekaman di GridView. Parameter @ProductName dan @UnitPrice berisi nilai baru yang dimasukkan oleh pengguna, sedangkan @original_ProductName dan @original_UnitPrice berisi nilai yang awalnya dimuat ke dalam GridView saat tombol Edit diklik:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Nota

Pernyataan ini UPDATE telah disederhanakan untuk keterbacaan. Dalam praktiknya UnitPrice , cek dalam WHERE klausul akan lebih terlibat karena UnitPrice dapat berisi NULL s dan memeriksa apakah NULL = NULL selalu mengembalikan False (sebagai gantinya Anda harus menggunakan IS NULL).

Selain menggunakan pernyataan mendasar UPDATE yang berbeda, mengonfigurasi TableAdapter untuk menggunakan konkurensi optimis juga memodifikasi tanda tangan metode langsung DB-nya. Ingat dari tutorial pertama kami, Membuat Lapisan Akses Data, bahwa metode akses langsung DB adalah metode yang menerima daftar nilai skalar sebagai parameter (bukan instans DataRow atau DataTable yang bertipe kuat). Saat menggunakan konkurensi optimis, metode langsung DB Update() dan Delete() menyertakan parameter input untuk nilai asli juga. Selain itu, kode di BLL untuk menggunakan pola pembaruan batch ( Update() metode kelebihan beban yang menerima DataRows dan DataTables daripada nilai skalar) juga harus diubah.

Daripada memperluas TableAdapters DAL kami yang ada untuk menggunakan konkurensi optimis (yang akan memerlukan perubahan pada BLL agar sesuai), mari kita buat Typed DataSet baru bernama NorthwindOptimisticConcurrency, dan tambahkan Products TableAdapter yang menggunakan konkurensi optimis. Setelah itu, kita akan membuat Lapisan Logika Bisnis kelas ProductsOptimisticConcurrencyBLL yang memiliki modifikasi yang sesuai untuk mendukung DAL konkurensi optimis. Setelah dasar ini diletakkan, kita akan siap untuk membuat halaman ASP.NET.

Langkah 2: Membuat Lapisan Akses Data yang Mendukung Konkurensi Optimis

Untuk membuat Himpunan Data Jenis baru, klik kanan folder DAL dalam App_Code folder dan tambahkan Himpunan Data baru bernama NorthwindOptimisticConcurrency. Seperti yang kita lihat di tutorial pertama, melakukannya akan menambahkan TableAdapter baru ke Typed DataSet, secara otomatis meluncurkan Wizard Konfigurasi TableAdapter. Di layar pertama, kita diminta untuk menentukan database untuk disambungkan - sambungkan ke database Northwind yang sama menggunakan pengaturan NORTHWNDConnectionString dari Web.config.

Menyambungkan ke Database Northwind yang Sama

Gambar 3: Sambungkan ke Database Northwind yang Sama (Klik untuk melihat gambar ukuran penuh)

Selanjutnya, kita diminta untuk mengkueri data: melalui pernyataan SQL ad-hoc, prosedur tersimpan baru, atau prosedur tersimpan yang sudah ada. Karena kami menggunakan kueri SQL ad-hoc di DAL asli kami, gunakan opsi ini di sini juga.

Tentukan Data yang Akan Diambil Menggunakan Pernyataan Ad-Hoc SQL

Gambar 4: Tentukan Data yang akan Diambil Menggunakan Pernyataan SQL Ad-Hoc (Klik untuk melihat gambar ukuran penuh)

Pada layar berikut, masukkan kueri SQL yang akan digunakan untuk mengambil informasi produk. Mari kita gunakan kueri SQL yang persis sama dengan yang digunakan untuk Products TableAdapter dari DAL asli kami, yang mengembalikan semua kolom Product beserta nama pemasok produk dan kategori.

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

Menggunakan Kueri SQL yang Sama dari TableAdapter Produk di DAL Asli

Gambar 5: Gunakan Kueri SQL yang Sama dari Products TableAdapter di DAL Asli (Klik untuk melihat gambar ukuran penuh)

Sebelum berpindah ke layar berikutnya, klik tombol Opsi Tingkat Lanjut. Agar TableAdapter ini menggunakan kontrol konkurensi optimis, cukup periksa kotak centang "Gunakan konkurensi optimis".

Aktifkan Kontrol Konkurensi Optimis dengan Memeriksa Kotak Centang

Gambar 6: Aktifkan Kontrol Konkurensi Optimis dengan Memeriksa Kotak Centang "Gunakan konkurensi optimis" (Klik untuk melihat gambar ukuran penuh)

Terakhir, tunjukkan bahwa TableAdapter harus menggunakan pola akses data yang baik mengisi DataTable maupun mengembalikan DataTable; juga tunjukkan bahwa metode langsung basis data harus dibuat. Ubah nama metode untuk pola Return a DataTable dari GetData ke GetProducts, sehingga dapat mencerminkan konvensi penamaan yang kami gunakan dalam DAL asli kami.

Memanfaatkan TableAdapter untuk Semua Pola Akses Data

Gambar 7: Minta TableAdapter Menggunakan Semua Pola Akses Data (Klik untuk melihat gambar ukuran penuh)

Setelah menyelesaikan wizard, Desainer DataSet akan menyertakan DataTable dan TableAdapter yang bertipe kuat Products. Luangkan waktu sejenak untuk mengganti nama DataTable dari Products menjadi ProductsOptimisticConcurrency, yang bisa Anda lakukan dengan mengklik kanan bilah judul DataTable dan memilih Ganti Nama dari menu konteks.

DataTable dan TableAdapter telah ditambahkan ke DataSet bertipe

Gambar 8: DataTable dan TableAdapter Telah Ditambahkan ke Himpunan Data Yang Ditik (Klik untuk melihat gambar ukuran penuh)

Untuk melihat perbedaan antara kueri UPDATE dan DELETE di TableAdapter ProductsOptimisticConcurrency (yang menggunakan konkurensi optimis) dan TableAdapter Produk (yang tidak), klik pada TableAdapter dan pergi ke jendela Properti. DeleteCommand Dalam subproperti properti UpdateCommand dan CommandText Anda dapat melihat sintaks SQL aktual yang dikirim ke database saat metode terkait pembaruan atau penghapusan DAL dipanggil. Untuk TableAdapter, pernyataan yang digunakan adalah: ProductsOptimisticConcurrency

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

DELETE Sedangkan pernyataan untuk TableAdapter Produk dalam DAL asli kami adalah yang jauh lebih sederhana:

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

Seperti yang Anda lihat, WHERE klausul dalam DELETE pernyataan untuk TableAdapter yang menggunakan konkurensi optimis mencakup perbandingan Product antara setiap nilai kolom tabel yang ada dan nilai asli pada saat GridView (atau DetailsView atau FormView) terakhir diisi. Karena semua bidang selain ProductID, ProductName, dan Discontinued dapat memiliki nilai NULL, parameter dan pemeriksaan tambahan disertakan untuk membandingkan nilai NULL dengan benar dalam klausa WHERE.

Kami tidak akan menambahkan DataTable tambahan ke Himpunan Data yang mendukung konkurensi optimis untuk tutorial ini, karena halaman ASP.NET kami hanya akan memberikan pembaruan dan penghapusan informasi produk. Namun, kita masih perlu menambahkan GetProductByProductID(productID) metode ke ProductsOptimisticConcurrency TableAdapter.

Untuk melakukannya, klik kanan pada bilah judul TableAdapter (area tepat di atas nama metode Fill dan GetProducts) dan pilih Tambah Kueri dari menu konteks. Ini akan meluncurkan Wizard Konfigurasi Kueri TableAdapter. Seperti konfigurasi awal TableAdapter kami, pilih untuk membuat GetProductByProductID(productID) metode menggunakan pernyataan SQL ad-hoc (lihat Gambar 4). GetProductByProductID(productID) Karena metode mengembalikan informasi tentang produk tertentu, tunjukkan bahwa kueri ini adalah SELECT jenis kueri yang mengembalikan baris.

Tandai Tipe Kueri sebagai

Gambar 9: Tandai Jenis Kueri sebagai "SELECT yang mengembalikan baris" (Klik untuk melihat gambar ukuran penuh)

Pada layar berikutnya kita diminta untuk menggunakan kueri SQL, dengan kueri default TableAdapter yang telah dimuat sebelumnya. Menambah kueri yang ada untuk menyertakan klausul WHERE ProductID = @ProductID, seperti yang ditunjukkan pada Gambar 10.

Menambahkan Klausul WHERE ke Kueri yang Telah Dimuat sebelumnya untuk Mengembalikan Rekaman Produk Tertentu

Gambar 10: Tambahkan WHERE Klausul ke Kueri yang Telah Dimuat sebelumnya untuk Mengembalikan Rekaman Produk Tertentu (Klik untuk melihat gambar ukuran penuh)

Terakhir, ubah nama metode yang dihasilkan menjadi FillByProductID dan GetProductByProductID.

Ganti nama Metode menjadi FillByProductID dan GetProductByProductID

Gambar 11: Ganti nama Metode menjadi FillByProductID dan GetProductByProductID (Klik untuk melihat gambar ukuran penuh)

Setelah wizard ini selesai, TableAdapter sekarang berisi dua metode untuk mengambil data: GetProducts(), yang mengembalikan semua produk; dan GetProductByProductID(productID), yang mengembalikan produk yang ditentukan.

Langkah 3: Membuat Lapisan Logika Bisnis untuk DAL Concurrency-Enabled Optimis

Kelas kami yang ada ProductsBLL memiliki contoh penggunaan pembaruan secara batch dan pola DB secara langsung. Metode AddProduct dan UpdateProduct overload keduanya menggunakan pola pembaruan batch, mengoper instans ProductRow ke metode Update milik TableAdapter. Metode DeleteProduct , di sisi lain, menggunakan pola langsung basis data (DB), memanggil metode TableAdapter Delete(productID).

Dengan TableAdapter ProductsOptimisticConcurrency yang baru, metode langsung DB sekarang memerlukan agar nilai awal juga diteruskan. Misalnya, metode Delete sekarang mengharapkan sepuluh parameter input: ProductID asli, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, dan Discontinued. Ini menggunakan nilai parameter input tambahan ini dalam WHERE klausul pernyataan DELETE yang dikirim ke database, hanya menghapus rekaman yang ditentukan jika nilai-nilai saat ini dari database sesuai dengan nilai asli.

Meskipun tanda tangan metode untuk metode TableAdapter Update yang digunakan dalam pola pembaruan batch belum berubah, kode yang diperlukan untuk merekam nilai asli dan baru. Oleh karena itu, daripada mencoba menggunakan DAL yang mendukung konkurensi optimis dengan kelas kami yang ada ProductsBLL , mari kita buat kelas Business Logic Layer baru untuk bekerja dengan DAL baru kami.

Tambahkan kelas bernama ProductsOptimisticConcurrencyBLL ke BLL folder dalam App_Code folder .

Tambahkan Kelas ProductsOptimisticConcurrencyBLL ke Folder BLL

Gambar 12: Tambahkan ProductsOptimisticConcurrencyBLL Kelas ke Folder BLL

Selanjutnya, tambahkan kode berikut ke ProductsOptimisticConcurrencyBLL kelas :

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

Perhatikan pernyataan penggunaan NorthwindOptimisticConcurrencyTableAdapters di atas awal deklarasi kelas. Namespace NorthwindOptimisticConcurrencyTableAdapters berisi ProductsOptimisticConcurrencyTableAdapter kelas , yang menyediakan metode DAL. Sebelum deklarasi kelas, Anda akan menemukan atribut System.ComponentModel.DataObject, yang menginstruksikan Visual Studio untuk menyertakan kelas ini dalam menu tarik-turun wizard ObjectDataSource.

Properti ProductsOptimisticConcurrencyBLL menyediakan akses cepat ke sebuah instans dari kelas Adapter, dan mengikuti pola yang digunakan dalam kelas-kelas BLL asli kami (ProductsOptimisticConcurrencyTableAdapter, ProductsBLL, dan sebagainya). Akhirnya, metode GetProducts() ini hanya memanggil metode DAL GetProducts() dan mengembalikan objek ProductsOptimisticConcurrencyDataTable yang berisi instans ProductsOptimisticConcurrencyRow untuk setiap rekaman produk dalam database.

Menghapus Produk Menggunakan Pola Langsung DB dengan Konkurensi Optimis

Saat menggunakan pola langsung DB terhadap DAL yang menggunakan konkurensi optimis, metode harus melewati nilai baru dan asli. Untuk menghapus, tidak ada nilai baru, jadi hanya nilai asli yang perlu diteruskan. Dalam BLL kami, maka, kita harus menerima semua parameter asli sebagai parameter input. Mari kita memiliki DeleteProduct metode di ProductsOptimisticConcurrencyBLL kelas menggunakan metode langsung DB. Ini berarti bahwa metode ini perlu menerima semua sepuluh bidang data produk sebagai parameter masukan, dan meneruskan ini ke DAL, seperti yang diperlihatkan dalam kode berikut:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

Jika nilai asli - nilai yang terakhir dimuat ke dalam GridView (atau DetailsView atau FormView) - berbeda dari nilai dalam database ketika pengguna mengklik tombol WHERE Hapus, klausa tidak akan cocok dengan rekaman database apa pun dan tidak ada rekaman yang akan terpengaruh. Oleh karena itu, metode TableAdapter Delete akan kembali 0 dan metode BLL DeleteProduct akan mengembalikan false.

Memperbarui Produk Menggunakan Pola Pembaruan Batch dengan Konkurensi Optimis

Seperti disebutkan sebelumnya, metode TableAdapter Update untuk pola pembaruan batch memiliki tanda tangan metode yang sama terlepas dari apakah konkurensi optimis digunakan atau tidak. Metode Update memerlukan DataRow, array DataRows, DataTable, atau Typed DataSet. Tidak ada parameter input tambahan untuk menentukan nilai asli. Ini dimungkinkan karena DataTable melacak nilai asli dan yang dimodifikasi untuk DataRow-nya. Ketika DAL mengeluarkan pernyataannya UPDATE , @original_ColumnName parameter diisi dengan nilai asli DataRow, sedangkan @ColumnName parameter diisi dengan nilai DataRow yang dimodifikasi.

ProductsBLL Di kelas (yang menggunakan DAL konkurensi asli dan tidak optimis), saat menggunakan pola pembaruan batch untuk memperbarui informasi produk, kode kami melakukan urutan peristiwa berikut:

  1. Membaca informasi produk terkini dari database ke dalam instance ProductRow menggunakan metode GetProductByProductID(productID) dari TableAdapter.
  2. Menetapkan nilai baru ke ProductRow instans dari Langkah 1
  3. Panggil metode TableAdapter Update, mengoper instance ProductRow.

Urutan langkah-langkah ini, bagaimanapun, tidak akan mendukung konkurensi optimis dengan benar karena ProductRow yang diisi di Langkah 1 diisi langsung dari database, yang berarti bahwa nilai asli yang digunakan oleh DataRow adalah yang saat ini ada di database, dan bukan yang terikat ke GridView pada awal proses pengeditan. Sebagai gantinya, saat menggunakan DAL yang mendukung konkurensi optimis, kita perlu mengubah UpdateProduct kelebihan metode untuk menggunakan langkah-langkah berikut:

  1. Membaca informasi produk terkini dari database ke dalam instance ProductsOptimisticConcurrencyRow menggunakan metode GetProductByProductID(productID) dari TableAdapter.
  2. Tetapkan nilai asli ke ProductsOptimisticConcurrencyRow instance dari Langkah 1
  3. ProductsOptimisticConcurrencyRow Panggil metode instansAcceptChanges(), yang menginstruksikan DataRow bahwa nilainya saat ini adalah yang "asli"
  4. Menetapkan nilai baru pada instans ProductsOptimisticConcurrencyRow
  5. Panggil metode TableAdapter Update, mengoper instance ProductsOptimisticConcurrencyRow.

Langkah 1 membaca semua nilai database saat ini untuk rekaman produk yang ditentukan. Langkah ini tidak diperlukan dalam pemanggilan fungsi berlebih yang memperbarui semua kolom produk (karena nilai-nilai ini tertumpuk di Langkah 2), tetapi sangat penting untuk pemanggilan fungsi berlebih di mana hanya sebagian nilai kolom yang diteruskan sebagai parameter input. Setelah nilai asli ditetapkan ke instans ProductsOptimisticConcurrencyRow, metode AcceptChanges() dipanggil, yang menandai nilai-nilai DataRow saat ini sebagai nilai asli yang akan digunakan dalam parameter di dalam pernyataan @original_ColumnName. Selanjutnya, nilai parameter baru ditetapkan ke ProductsOptimisticConcurrencyRow dan, akhirnya, metode Update dipanggil dengan meneruskan DataRow.

Kode berikut menunjukkan UpdateProduct kelebihan beban yang menerima semua bidang data produk sebagai parameter input. Meskipun tidak ditampilkan di sini, kelas ProductsOptimisticConcurrencyBLL yang disertakan dalam unduhan untuk tutorial ini juga memiliki overload UpdateProduct yang hanya menerima nama produk dan harga sebagai parameter input.

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
    // STEP 2: Assign the original values to the product instance
    AssignAllProductValues(product, original_productName, original_supplierID,
        original_categoryID, original_quantityPerUnit, original_unitPrice,
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
        original_discontinued);
    // STEP 3: Accept the changes
    product.AcceptChanges();
    // STEP 4: Assign the new values to the product instance
    AssignAllProductValues(product, productName, supplierID, categoryID,
        quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
        discontinued);
    // STEP 5: Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

Langkah 4: Meneruskan Nilai Asli dan Baru Dari Halaman ASP.NET ke Metode BLL

Dengan DAL dan BLL selesai, yang tersisa adalah membuat halaman ASP.NET yang dapat menggunakan logika konkurensi optimis yang dibangun dalam sistem. Secara khusus, kontrol Web data (GridView, DetailsView, atau FormView) harus mengingat nilai aslinya dan ObjectDataSource harus meneruskan kedua set nilai ke Lapisan Logika Bisnis. Selain itu, halaman ASP.NET harus dikonfigurasi untuk menangani pelanggaran konkurensi dengan baik.

Mulailah dengan membuka halaman OptimisticConcurrency.aspx di folder EditInsertDelete dan menambahkan GridView ke Desainer, lalu mengatur propertinya ID ke ProductsGrid. Dari tag pintar GridView, pilih untuk membuat ObjectDataSource baru bernama ProductsOptimisticConcurrencyDataSource. Karena kami ingin ObjectDataSource ini menggunakan DAL yang mendukung konkurensi optimis, konfigurasikan untuk menggunakan ProductsOptimisticConcurrencyBLL objek .

Biarkan ObjectDataSource Menggunakan Objek ProductsOptimisticConcurrencyBLL

Gambar 13: Minta ObjectDataSource Menggunakan ProductsOptimisticConcurrencyBLL Objek (Klik untuk melihat gambar ukuran penuh)

Pilih metode GetProducts, UpdateProduct, dan DeleteProduct dari daftar drop-down dalam wizard. Untuk metode UpdateProduct, gunakan kelebihan beban yang menerima semua bidang data produk.

Mengatur Properti Pengendali ObjectDataSource

Setelah menyelesaikan wizard, markup deklaratif ObjectDataSource akan terlihat seperti berikut ini:

<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

Seperti yang Anda lihat, DeleteParameters koleksi berisi Parameter instans untuk masing-masing dari sepuluh parameter masukan dalam metode ProductsOptimisticConcurrencyBLL dari kelas DeleteProduct. Demikian juga, UpdateParameters koleksi berisi Parameter instance untuk setiap parameter input di UpdateProduct.

Untuk tutorial sebelumnya yang melibatkan modifikasi data, kami akan menghapus properti ObjectDataSource OldValuesParameterFormatString pada saat ini, karena properti ini menunjukkan bahwa metode BLL mengharapkan nilai lama (atau asli) untuk diteruskan serta nilai baru. Selain itu, nilai properti ini menunjukkan nama parameter input untuk nilai asli. Karena kita meneruskan nilai asli ke BLL, jangan hapus properti ini.

Nota

Nilai dari properti OldValuesParameterFormatString harus dipetakan ke nama parameter input yang ada di BLL yang mengharapkan nilai asli. Karena kami menamai parameter original_productName ini, original_supplierID, dan sebagainya, Anda dapat membiarkan nilai properti OldValuesParameterFormatString sebagai original_{0}. Namun, jika parameter input metode BLL memiliki nama seperti old_productName, old_supplierID, dan sebagainya, Anda harus memperbarui OldValuesParameterFormatString properti ke old_{0}.

Ada satu pengaturan properti akhir yang perlu dibuat agar ObjectDataSource meneruskan nilai asli dengan benar ke metode BLL. ObjectDataSource memiliki properti ConflictDetection yang dapat ditetapkan ke salah satu dari dua nilai:

  • OverwriteChanges - nilai default; tidak mengirim nilai asli ke parameter input asli metode BLL
  • CompareAllValues - mengirim nilai asli ke metode BLL; pilih opsi ini saat menggunakan konkurensi optimis

Sempatkan sejenak untuk mengatur properti ConflictDetection ke CompareAllValues.

Mengonfigurasi Properti dan Bidang dari GridView

Dengan properti ObjectDataSource yang dikonfigurasi dengan benar, mari kita alihkan perhatian kita untuk menyiapkan GridView. Pertama, karena kita ingin GridView mendukung pengeditan dan penghapusan, klik kotak centang Aktifkan Pengeditan dan Aktifkan Penghapusan dari tag pintar GridView. Ini akan menambahkan CommandField yang ShowEditButton dan ShowDeleteButton keduanya diatur ke true.

Saat terikat ke ProductsOptimisticConcurrencyDataSource ObjectDataSource, GridView berisi bidang untuk setiap bidang data produk. Meskipun GridView seperti itu dapat diedit, pengalaman pengguna sama sekali tidak memuaskan. BoundFields CategoryID dan SupplierID akan dirender sebagai Kotak Teks, mengharuskan pengguna untuk memasukkan kategori dan pemasok yang sesuai dalam bentuk nomor ID. Tidak akan ada pemformatan untuk bidang numerik dan tidak ada kontrol validasi untuk memastikan bahwa nama produk telah disediakan dan bahwa harga satuan, unit dalam stok, unit berdasarkan pesanan, dan nilai tingkat pengurutan ulang keduanya adalah nilai numerik yang tepat dan lebih besar dari atau sama dengan nol.

Seperti yang kita bahas dalam Menambahkan Kontrol Validasi ke Antarmuka Pengeditan dan Penyisipan dan Menyesuaikan tutorial Antarmuka Modifikasi Data , antarmuka pengguna dapat disesuaikan dengan mengganti BoundFields dengan TemplateFields. Saya telah memodifikasi GridView ini dan antarmuka pengeditannya dengan cara berikut:

  • Menghapus ProductID, SupplierName, dan CategoryName BoundFields
  • Mengonversi ProductName BoundField ke TemplateField dan menambahkan kontrol RequiredFieldValidation.
  • Mengubah CategoryID dan SupplierID BoundFields menjadi TemplateFields, serta menyesuaikan antarmuka pengeditan agar menggunakan DropDownLists alih-alih TextBoxes. Dalam TemplateFields ItemTemplates ini, CategoryName dan SupplierName bidang data ditampilkan.
  • Mengonversi UnitPrice, , UnitsInStockUnitsOnOrder, dan ReorderLevel BoundFields ke TemplateFields dan menambahkan kontrol CompareValidator.

Karena kita sudah memeriksa cara menyelesaikan tugas-tugas ini dalam tutorial sebelumnya, saya hanya akan mencantumkan sintaks deklaratif akhir di sini dan meninggalkan implementasi sebagai praktik.

<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

Kami hampir mencapai contoh yang berfungsi sepenuhnya. Namun, ada beberapa nuansa yang akan muncul dan menyebabkan masalah bagi kita. Selain itu, kami masih memerlukan beberapa antarmuka yang memperingatkan pengguna ketika pelanggaran konkurensi telah terjadi.

Nota

Agar kontrol Web data meneruskan nilai asli dengan benar ke ObjectDataSource (yang kemudian diteruskan ke BLL), sangat penting bahwa properti GridView EnableViewState diatur ke true (default). Jika Anda menonaktifkan status tampilan, nilai asli akan hilang saat postback.

Meneruskan Nilai Asli yang Benar ke ObjectDataSource

Ada beberapa masalah dengan cara GridView dikonfigurasi. Jika properti ObjectDataSource diatur ke ConflictDetection (seperti milik kita), ketika metode CompareAllValues atau Update() dari ObjectDataSource dipanggil oleh GridView (atau DetailsView atau FormView), ObjectDataSource mencoba menyalin nilai asli GridView ke dalam instans Delete() yang sesuai.Parameter Lihat kembali ke Gambar 2 untuk representasi grafis dari proses ini.

Secara khusus, nilai asli GridView diberi nilai dalam pernyataan pengikatan data dua arah setiap kali data terikat ke GridView. Oleh karena itu, sangat penting bahwa nilai asli yang diperlukan semuanya diambil melalui pengikatan data dua arah dan disediakan dalam format yang dapat dikonversi.

Untuk melihat mengapa ini penting, luangkan waktu sejenak untuk mengunjungi halaman kami di browser. Seperti yang diharapkan, GridView mencantumkan setiap produk dengan tombol Edit dan Hapus di kolom paling kiri.

Produk Tercantum dalam GridView

Gambar 14: Produk Tercantum dalam GridView (Klik untuk melihat gambar ukuran penuh)

Jika Anda mengklik tombol Hapus untuk produk apa pun, kesalahan FormatException akan terjadi.

Setiap upaya menghapus produk mengakibatkan FormatException

Gambar 15: Mencoba Menghapus Hasil Produk Apa Pun dalam (FormatExceptionKlik untuk melihat gambar ukuran penuh)

FormatException dinaikkan ketika ObjectDataSource mencoba membaca nilai asli UnitPrice. ItemTemplate Karena UnitPrice diformat sebagai mata uang (<%# Bind("UnitPrice", "{0:C}") %>), termasuk simbol mata uang seperti $19,95. Terjadi FormatException saat ObjectDataSource mencoba mengonversi string ini menjadi decimal. Untuk menghindari masalah ini, kami memiliki sejumlah opsi:

  • Hapus pemformatan mata uang dari ItemTemplate. Artinya, alih-alih menggunakan <%# Bind("UnitPrice", "{0:C}") %>, cukup gunakan <%# Bind("UnitPrice") %>. Kelemahannya adalah harga tidak lagi diformat.
  • Tampilkan UnitPrice yang diformat sebagai mata uang di ItemTemplate, gunakan kata kunci Eval untuk melakukannya. Ingat yang Eval melakukan pengikatan data satu arah. Kita masih perlu memberikan UnitPrice nilai untuk nilai asli, jadi kita masih memerlukan pernyataan pengikatan data dua arah di ItemTemplate, tetapi ini dapat ditempatkan dalam kontrol Web Label yang propertinya Visible diatur ke false. Kita dapat menggunakan markup berikut di ItemTemplate:
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • Hapus pemformatan mata uang dari ItemTemplate, menggunakan <%# Bind("UnitPrice") %>. Di penanganan acara GridView RowDataBound, akses kontrol Web Label dengan cara terprogram di mana nilai UnitPrice ditampilkan dan atur properti Text ke versi yang telah diformat.
  • Biarkan UnitPrice tetap diformat sebagai mata uang. Di penanganan aktivitas GridView RowDeleting , ganti nilai asli UnitPrice yang ada ($ 19,95) dengan nilai desimal aktual menggunakan Decimal.Parse. Kami melihat cara mencapai sesuatu yang mirip dalam RowUpdating penanganan acara di tutorial Penanganan Pengecualian BLL dan DAL-Level di Halaman ASP.NET.

Untuk contoh saya, saya memilih untuk menggunakan pendekatan kedua, menambahkan kontrol Web Label tersembunyi yang propertinya Text adalah data dua arah yang terikat ke nilai yang tidak diformat UnitPrice .

Setelah memecahkan masalah ini, coba klik tombol Hapus untuk produk apa pun lagi. Kali ini Anda akan mendapatkan InvalidOperationException ketika ObjectDataSource mencoba memanggil metode BLL UpdateProduct .

ObjectDataSource Tidak Dapat Menemukan Metode dengan Parameter Input yang Ingin Dikirimnya

Gambar 16: ObjectDataSource Tidak Dapat Menemukan Metode dengan Parameter Input yang Ingin Dikirimnya (Klik untuk melihat gambar ukuran penuh)

Melihat pesan pengecualian, jelas bahwa ObjectDataSource ingin memanggil metode BLL DeleteProduct yang menyertakan parameter masukan original_CategoryName dan original_SupplierName. Ini karena ItemTemplate s untuk CategoryID dan SupplierID TemplateFields saat ini berisi pernyataan Ikat dua arah dengan CategoryName bidang data dan SupplierName . Sebagai gantinya, kita perlu menyertakan pernyataan Bind dengan bidang data CategoryID dan SupplierID. Untuk mencapai hal ini, ganti pernyataan Ikatan yang ada dengan Eval pernyataan, lalu tambahkan kontrol Label tersembunyi yang propertinya Text terikat ke CategoryID bidang data dan SupplierID menggunakan pengikatan data dua arah, seperti yang ditunjukkan di bawah ini:

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

Dengan perubahan ini, kami sekarang dapat berhasil menghapus dan mengedit informasi produk! Di Langkah 5 kita akan melihat cara memverifikasi bahwa pelanggaran konkurensi sedang terdeteksi. Tetapi untuk saat ini, luangkan waktu beberapa menit untuk mencoba memperbarui dan menghapus beberapa catatan untuk memastikan bahwa memperbarui dan menghapus untuk satu pengguna berfungsi seperti yang diharapkan.

Langkah 5: Menguji Dukungan Konkurensi Optimis

Untuk memverifikasi bahwa pelanggaran konkurensi sedang terdeteksi (daripada mengakibatkan data ditimpa secara membabi buta), kita perlu membuka dua jendela browser pada halaman ini. Di kedua instans browser, klik tombol Edit untuk Chai. Kemudian, hanya di salah satu browser, ubah nama menjadi "Teh Chai" dan klik Perbarui. Pembaruan harus berhasil dan mengembalikan GridView ke status pra-pengeditannya, dengan "Chai Tea" sebagai nama produk baru.

Namun, di instans jendela browser lainnya, nama produk TextBox masih menampilkan "Chai". Di jendela browser kedua ini, perbarui ke UnitPrice25.00. Tanpa dukungan konkurensi optimis, mengklik pembaruan di instans browser kedua akan mengubah nama produk kembali ke "Chai", sehingga menimpa perubahan yang dilakukan oleh instans browser pertama. Namun, ketika konkurensi optimis digunakan, mengklik tombol Perbarui pada instans browser kedua akan menghasilkan DBConcurrencyException.

Ketika Pelanggaran Konkurensi Terdeteksi, DBConcurrencyException dilemparkan

Gambar 17: Saat pelanggaran kesamaan akses terdeteksi, sebuah DBConcurrencyException dilemparkan (Klik untuk melihat gambar ukuran penuh)

hanya DBConcurrencyException dilemparkan ketika pola pembaruan batch DAL digunakan. Pola langsung DB tidak menimbulkan pengecualian, itu hanya menunjukkan bahwa tidak ada baris yang terpengaruh. Untuk mengilustrasikan hal ini, kembalikan GridView kedua instans browser ke status pra-pengeditannya. Selanjutnya, di instans browser pertama, klik tombol Edit dan ubah nama produk dari "Chai Tea" kembali ke "Chai" dan klik Perbarui. Di jendela browser kedua, klik tombol Hapus untuk Chai.

Setelah mengklik Hapus, halaman dimuat ulang, GridView memanggil metode Delete() dari ObjectDataSource, dan ObjectDataSource memanggil metode ProductsOptimisticConcurrencyBLL dari kelas DeleteProduct, sambil meneruskan nilai asli. Nilai asli ProductName untuk instans browser kedua adalah "Chai Tea", yang tidak cocok dengan nilai saat ini ProductName dalam database. Oleh karena itu, pernyataan yang dikeluarkan oleh DELETE untuk database tidak memengaruhi baris mana pun karena tidak ada catatan dalam database yang sesuai dengan klausa WHERE. Metode DeleteProduct mengembalikan false dan data ObjectDataSource di-rebound ke GridView.

Dari perspektif pengguna akhir, mengklik tombol Hapus untuk Teh Chai di jendela browser kedua menyebabkan layar berkedip dan, setelah kembali, produk masih ada, meskipun sekarang terdaftar sebagai "Chai" (perubahan nama produk yang dilakukan oleh instans browser pertama). Jika pengguna mengklik tombol Hapus lagi, Hapus akan berhasil, karena nilai asli ProductName GridView ("Chai") sekarang cocok dengan nilai dalam database.

Dalam kedua kasus ini, pengalaman pengguna jauh dari ideal. Kami jelas tidak ingin menunjukkan kepada pengguna detail nitty-gritty pengecualian DBConcurrencyException saat menggunakan pola pembaruan batch. Dan perilaku saat menggunakan pola langsung DB agak membingungkan karena perintah pengguna gagal, tetapi tidak ada indikasi yang tepat tentang mengapa.

Untuk memperbaiki kedua masalah ini, kita dapat membuat kontrol Label Web pada halaman yang memberikan penjelasan mengapa pembaruan atau penghapusan gagal. Untuk pola pembaruan batch, kita dapat menentukan apakah suatu pengecualian terjadi di penanganan acara tingkat lanjut GridView, menampilkan label peringatan sesuai kebutuhan. Untuk metode langsung DB, kita dapat memeriksa nilai pengembalian metode BLL (yaitu true jika satu baris terpengaruh, false jika tidak) dan menampilkan pesan informasi sesuai kebutuhan.

Langkah 6: Menambahkan Pesan Informasi dan Menampilkannya dalam Menghadapi Pelanggaran Konkurensi

Ketika terjadi pelanggaran pengaksesan bersamaan, perilaku yang ditunjukkan tergantung pada apakah pembaruan batch DAL atau pola langsung ke basis data digunakan. Tutorial kami menggunakan kedua pola, dengan pola pembaruan batch yang digunakan untuk memperbarui dan pola langsung DB yang digunakan untuk menghapus. Untuk memulai, mari kita tambahkan dua kontrol Web Label ke halaman kami yang menjelaskan bahwa pelanggaran konkurensi terjadi saat mencoba menghapus atau memperbarui data. Atur properti Visible dan EnableViewState kontrol Label ke false; hal ini akan membuat mereka tersembunyi di setiap kunjungan halaman kecuali untuk kunjungan halaman tertentu di mana properti Visible mereka diatur secara terprogram ke true.

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

Selain mengatur properti Visible, EnabledViewState, dan Text mereka, saya juga telah mengatur properti CssClass ke Warning, yang menyebabkan Label ditampilkan dalam font besar, merah, miring, tebal. Kelas CSS Warning ini didefinisikan dan ditambahkan ke Styles.css kembali dalam tutorial Memeriksa Peristiwa yang Terkait dengan Menyisipkan, Memperbarui, dan Menghapus .

Setelah menambahkan Label ini, Perancang di Visual Studio akan terlihat mirip dengan Gambar 18.

Dua Kontrol Label Telah Ditambahkan ke Halaman

Gambar 18: Dua Kontrol Label Telah Ditambahkan ke Halaman (Klik untuk melihat gambar ukuran penuh)

Dengan kontrol Web Label ini di tempat, kami siap untuk memeriksa cara menentukan kapan pelanggaran konkurensi telah terjadi, di mana properti Label Visible yang sesuai dapat diatur ke true, menampilkan pesan informasi.

Menangani Pelanggaran Konkurensi Saat Memperbarui

Mari kita lihat terlebih dahulu cara menangani pelanggaran konkurensi saat menggunakan pola pembaruan batch. Karena pelanggaran tersebut dengan pola pembaruan batch menyebabkan DBConcurrencyException pengecualian dilemparkan, kita perlu menambahkan kode ke halaman ASP.NET kita untuk menentukan apakah DBConcurrencyException pengecualian terjadi selama proses pembaruan. Jika demikian, kita harus menampilkan pesan kepada pengguna yang menjelaskan bahwa perubahan mereka tidak disimpan karena pengguna lain telah memodifikasi data yang sama antara ketika mereka mulai mengedit rekaman dan ketika mereka mengklik tombol Perbarui.

Seperti yang kita lihat dalam tutorial Penanganan Pengecualian BLL dan DAL-Level di Halaman ASP.NET, pengecualian tersebut dapat dideteksi dan ditekan dalam penanganan peristiwa tingkat akhir kontrol Web data. Oleh karena itu, kita perlu membuat pengendali acara untuk peristiwa GridView RowUpdated yang memeriksa apakah pengecualian DBConcurrencyException telah dilemparkan. Pengendali acara ini diteruskan referensi ke pengecualian apa pun yang terjadi selama pemrosesan pembaruan, seperti yang ditunjukkan pada kode pengendali acara di bawah ini.

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

Dalam menghadapi DBConcurrencyException pengecualian, penanganan aktivitas ini menampilkan UpdateConflictMessage kontrol Label dan menunjukkan bahwa pengecualian telah ditangani. Dengan kode ini diberlakukan, ketika pelanggaran konkurensi terjadi saat memperbarui rekaman, perubahan pengguna hilang, karena mereka akan menimpa modifikasi pengguna lain pada saat yang sama. Secara khusus, GridView dikembalikan ke status pra-pengeditannya dan terikat ke data database saat ini. Ini akan memperbarui baris GridView dengan perubahan pengguna lain, yang sebelumnya tidak terlihat. Selain itu, UpdateConflictMessage kontrol Label akan menjelaskan kepada pengguna apa yang baru saja terjadi. Urutan peristiwa ini dirinci dalam Gambar 19.

Pembaruan Pengguna Hilang dalam Menghadapi Pelanggaran Konkurensi

Gambar 19: Pembaruan pengguna hilang karena pelanggaran konkruensi (Klik untuk melihat gambar ukuran penuh)

Nota

Atau, daripada mengembalikan GridView ke keadaan sebelum diedit, kita dapat meninggalkan GridView dalam keadaan sedang diedit dengan mengatur properti KeepInEditMode dari objek GridViewUpdatedEventArgs yang diteruskan ke true. Namun, jika Anda mengambil pendekatan ini, pastikan untuk mengikat ulang data ke GridView (dengan memanggil metodenya DataBind() ) sehingga nilai pengguna lain dimuat ke dalam antarmuka pengeditan. Kode yang tersedia untuk diunduh dengan tutorial ini memiliki dua baris kode di penangan peristiwa RowUpdated yang diberi tanda komentar; cukup hapus tanda komentar pada baris kode ini agar GridView tetap dalam mode edit setelah pelanggaran konkurensi.

Menanggapi Pelanggaran Konkurensi Saat Menghapus

Dengan pola langsung DB, tidak ada pengecualian yang dimunculkan dalam menghadapi pelanggaran konkurensi. Sebaliknya, pernyataan database tidak memengaruhi rekaman mana pun, karena klausa WHERE tidak sesuai dengan rekaman mana pun. Semua metode modifikasi data yang dibuat di BLL telah dirancang sedemikian rupa sehingga mengembalikan nilai Boolean yang menunjukkan apakah mereka mempengaruhi tepat satu catatan atau tidak. Oleh karena itu, untuk menentukan apakah pelanggaran konkurensi terjadi saat menghapus rekaman, kita dapat memeriksa nilai pengembalian metode BLL DeleteProduct .

Nilai pengembalian untuk metode BLL dapat diperiksa dalam penangan kejadian tingkat lanjut ObjectDataSource melalui properti ReturnValue dari objek ObjectDataSourceStatusEventArgs yang diteruskan ke penangan kejadian. Karena kami tertarik untuk menentukan nilai pengembalian dari metode DeleteProduct, kami perlu membuat penanganan peristiwa untuk peristiwa ObjectDataSource Deleted. Properti ReturnValue berjenis object dan mungkin null jika pengecualian terjadi dan metode terganggu sebelum dapat mengembalikan nilai. Oleh karena itu, pertama-tama kita harus memastikan bahwa ReturnValue properti tersebut bukan null dan merupakan nilai Boolean. Mengandaikan pemeriksaan ini berhasil, kami menunjukkan kontrol Label DeleteConflictMessage jika ReturnValuefalse. Ini dapat dicapai dengan menggunakan kode berikut:

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

Dalam menghadapi pelanggaran konkurensi, permintaan penghapusan pengguna dibatalkan. GridView disegarkan, memperlihatkan perubahan yang terjadi untuk rekaman tersebut antara waktu pengguna memuat halaman dan ketika ia mengklik tombol Hapus. Ketika pelanggaran seperti itu DeleteConflictMessage terjadi, Label ditampilkan, menjelaskan apa yang baru saja terjadi (lihat Gambar 20).

Penghapusan oleh Pengguna Dibatalkan karena Pelanggaran Kesamaan Waktu

Gambar 20: Pembatalan Penghapusan Pengguna karena Pelanggaran Ketersamaan (Klik untuk melihat gambar ukuran penuh)

Ringkasan

Peluang untuk pelanggaran konkurensi ada di setiap aplikasi yang memungkinkan beberapa pengguna bersamaan untuk memperbarui atau menghapus data. Jika pelanggaran tersebut tidak diperhitungkan, ketika dua pengguna secara bersamaan memperbarui data yang sama, siapa pun yang melakukan penulisan terakhir "menang," menggantikan perubahan yang dibuat oleh pengguna lainnya. Atau, pengembang dapat menerapkan kontrol konkurensi yang optimis atau pesimis. Kontrol konkurensi optimis mengasumsikan bahwa pelanggaran konkurensi jarang terjadi dan hanya melarang perintah pembaruan atau penghapusan yang akan merupakan pelanggaran konkurensi. Kontrol konkurensi pesimis mengasumsikan bahwa pelanggaran konkurensi sering terjadi dan hanya menolak perintah pembaruan atau penghapusan satu pengguna tidak dapat diterima. Dengan kontrol konkurensi pesimis, memperbarui rekaman melibatkan penguncian, sehingga mencegah pengguna lain memodifikasi atau menghapus rekaman saat dikunci.

Typed DataSet di .NET menyediakan fungsionalitas untuk mendukung kontrol konkurensi optimis. Secara khusus, pernyataan UPDATE dan DELETE yang dikeluarkan untuk database menyertakan semua kolom tabel, sehingga memastikan bahwa pembaruan atau penghapusan hanya akan terjadi jika data catatan saat ini cocok dengan data asli yang dimiliki pengguna saat melakukan pembaruan atau penghapusan mereka. Setelah DAL dikonfigurasi untuk mendukung konkurensi optimis, metode BLL perlu diperbarui. Selain itu, halaman ASP.NET yang memanggil ke bawah ke BLL harus dikonfigurasi sehingga ObjectDataSource mengambil nilai asli dari kontrol Web datanya dan meneruskannya ke BLL.

Seperti yang kita lihat dalam tutorial ini, menerapkan kontrol konkurensi optimis dalam aplikasi web ASP.NET melibatkan pembaruan DAL dan BLL dan menambahkan dukungan di halaman ASP.NET. Apakah pekerjaan tambahan ini merupakan investasi bijak dari waktu dan upaya Anda tergantung pada aplikasi Anda. Jika Anda jarang memiliki pengguna bersamaan yang memperbarui data, atau data yang mereka perbarui berbeda satu sama lain, kontrol konkurensi bukanlah masalah utama. Namun, jika Anda secara rutin memiliki beberapa pengguna di situs Anda yang bekerja dengan data yang sama, kontrol konkurensi dapat membantu mencegah pembaruan atau penghapusan satu pengguna secara tidak disadari menimpa pengguna lain.

Selamat Pemrograman!

Tentang Penulis

Scott Mitchell, penulis tujuh buku ASP/ASP.NET dan pendiri 4GuysFromRolla.com, telah bekerja sama dengan teknologi Microsoft Web sejak 1998. Scott bekerja sebagai konsultan, pelatih, dan penulis independen. Buku terbarunya adalah Sams Teach Yourself ASP.NET 2.0 dalam 24 Jam. Dia dapat dijangkau di mitchell@4GuysFromRolla.com.