Proyek Blazor Server #6: Membuat Komponen Dialog Reusable

Diimplementasikan untuk konfirmasi penghapusan dan update (pembaruan)

M. Ramadhan
Telematika
10 min readJul 22, 2021

--

Daftar Isi

Pendahuluan
Membuat Komponen Dialog
Implementasi Konfirmasi Penghapusan
Implementasi Konfirmasi Update
Bagaimana Cara Kerjanya?
Referensi

Photo by Gabriela Juri from Burst

Semua pembahasan berikut berdasarkan proyek di artikel sebelumnya, Proyek Blazor Server #5. Silakan download source codenya di bawah ini.

Pendahuluan

Sebagian pengguna tidak menyukai konfirmasi penghapusan dan konfirmasi update. Bagi mereka, hal ini tidak berguna, bahkan hanya menghambat kelancaran pekerjaan. Namun, bagi sebagian lainnya, hal ini sangat bermanfaat untuk mencegah ketidaksengajaan menghapus maupun memperbaharui data.

Sebagai contoh, berikut adalah tangkapan layar daftar pengarang.

Ketika pengguna mengklik salah satu tombol Delete, bila tidak ada konfirmasi, datanya langsung terhapus dari basis data dan tidak ada cara mudah untuk mengembalikannya. Namun bila ada konfirmasi, terlebih dahulu tampil pesan berikut.

Begitu pula pembaruan data pengarang berikut.

Ketika pengguna mengklik tombol Save, bila tidak ada konfirmasi, datanya langsung tersimpan ke dalam basis data. Bila ada kesalahan data, pengguna harus mengedit ulang datanya. Namun bila ada konfirmasi, terlebih dahulu tampil pesan berikut.

Selain pengarang, masih ada data buku dan penerbit yang juga memerlukan konfirmasi penghapusan dan konfirmasi update sehingga total ada enam konfirmasi. Walaupun tidak salah, membuat enam buah konfirmasi sangat tidak efisien. Bagaimana kalau ada tambahan data lain, misalnya data anggota, data peminjaman, dsb., apakah harus dibuat lagi konfirmasi tambahannya?

Ada kesamaan format tampilan pada dua contoh konfirmasi di atas. Daripada membuat enam komponen dialog yang berbeda, akan lebih efisien bila membuat sebuah komponen dialog yang dapat digunakan kembali tanpa batas.

Membuat Komponen Dialog

(1) Membuka aplikasi BookApp.

  • Buka Visual Studio
  • Klik BookApp.sln untuk membuka aplikasinya.

(2) Membuat folder Component.

  • Pada jendela Solution Explorer, klik kanan BookApp.
  • Pilih submenu: Add|New Folder.
  • Ketik Components sebagai nama foldernya.

(3) Membuat file komponen Dialog.

  • Pada jendela Solution Explorer, klik kanan folder Components.
  • Pilih submenu Add|Razor Component...
  • Klik Razor Component.
  • Di kotak teks Name, ketik Dialog.razor sebagai nama filenya.
  • Klik tombol Add.
  • Klik ganda Dialog.razor untuk membuka filenya.

(4) Kode sumber

Berdasarkan gambar di atas, komponen dialog berisi properti berikut:
(1) Caption berisi teks judul.
(2) Message berisi teks pesan.
(3) Category merupakan jenis dialog.

Ada dua tombol yang kalau diklik memunculkan event (kejadian) yang memicu metode. Klik tombol pertama untuk menyetujui aksi yang akan dilakukan. Sebaliknya, klik tombol kedua untuk membatalkan aksi. Dengan demikian, selain tiga properti di atas, komponen dialog berisi dua metode, yaitu:
(4) Metode OK untuk melaksanakan aksi.
(5) Metode Cancel untuk membatalkan aksi.

Berikut adalah kode sumbernya.

Dialog.razor

<div class="modal fade show" id="myModal" style="display:block; background-color: rgba(10,10,10,.8);" aria-modal="true" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">@Caption</h4>
<button type="button" class="close"
@onclick="@Cancel">&times;</button>
</div>
<div class="modal-body">
<p>@Message</p>
</div>
<div class="modal-footer">
@switch (Type)
{
case Category.Okay:
<button type="button" class="btn btn-primary"
@onclick=@Ok>OK</button>
break;
case Category.SaveNot:
<button type="button" class="btn btn-primary"
@onclick=@Ok>Save</button>
<button type="button" class="btn btn-warning"
@onclick="@Cancel">Don't Save</button>
break;
case Category.DeleteNot:
<button type="button" class="btn btn-danger"
@onclick=@Ok>Delete</button>
<button type="button" class="btn btn-warning"
@onclick="@Cancel">Don't Delete</button>
break;
}
</div>
</div>
</div>
</div>
@code {
[Parameter] public string Caption { get; set; }
[Parameter] public string Message { get; set; }
[Parameter] public EventCallback<bool> OnClose { get; set; }
[Parameter] public Category Type { get; set; }
private Task Cancel()
{
return OnClose.InvokeAsync(false);
}
private Task Ok()
{
return OnClose.InvokeAsync(true);
}
public enum Category
{
Okay,
SaveNot,
DeleteNot
}
}

Pada kode sumber di atas, ada properti ke-4 yaitu OnClose sebagai EventCallback sehingga Dialog.razor sebagai komponen anak dapat mengirimkan argumen ke komponen induk. Dalam kode di atas, argumen memiliki nilai:

  • true bila pengguna mengklik tombol pertama dan memicu motode Ok().
  • false bila pengguna mengklik tombol kedua yang kemudian memicu motode Cancel().

Untuk menggunakan komponen Dialog.razor dalam aplikasi, tambahkan kode berikut ke dalam_Imports.razor.

@using BookApp.Components

Jadi kode lengkapnya adalah sebagai berikut:

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BookApp
@using BookApp.Shared
@using BookApp.Data
@using BookApp.Interfaces
@using BookApp.Entities
@using BookApp.Components
@using System.Numerics

Teks yang dicetak tebal adalah kode tambahan.

Implementasi Konfirmasi Penghapusan

Sebagai contoh, kita akan mengimplementasikan konfirmasi penghapusan data author (pengarang).

  • Pada jendela Solution Explorer, buka folder Pages.
  • Klik kanan ListAuthor.razor, lalu pilih submenu Open.
  • Lakukan modifikasi isi fileListAuthor.razor sehingga kode selengkapnya adalah sebagai berikut.

ListAuthor.razor

@page "/listAuthor"
@inject IAuthorService authorService
<link href="https://stackpath.bootstrapcdn.com/
font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
.sort-th {
cursor: pointer;
}
.fa {
float: right;
}
.btn-custom {
color: black;
float: left;
padding: 8px 16px;
text-decoration: none;
transition: background-color .3s;
border: 2px solid #000;
margin: 0px 5px 0px 5px;
}
</style>
<a class="btn btn-primary" href='/addAuthor/0'>Add new author</a>@if (authors == null)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row col-md-3 pull-right">
<input type="text" id="txtSearch"
placeholder="Search Names..." class="form-control"
@bind="SearchTerm" @bind:event="oninput" />
</div>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th class="sort-th" @onclick="@(() => SortTable("Id"))">
Id<span class="fa @(SetSortIcon("Id"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("FName"))">
First Name
<span class="fa @(SetSortIcon("FName"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("LName"))">
Last Name
<span class="fa @(SetSortIcon("LName"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("Phone"))">
Phone
<span class="fa @(SetSortIcon("Phone"))"></span>
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@if (authors == null || authors.Count == 0)
{
<tr>
<td colspan="3">
No Records to display
</td>
</tr>
}
else
{
foreach (var author in authors)
{
<tr>
<td>
<hr style="padding:0px; margin:0px;">
@author.Id
</td>
<td>
<hr style="padding:0px; margin:0px;">
@author.FName
</td>
<td>
<hr style="padding:0px; margin:0px;">
@author.LName
</td>
<td>
<hr style="padding:0px; margin:0px;">
@author.Phone
</td>
<td>
<hr style="padding:0px; margin:0px;">
<a class="btn btn-primary"
href='/editAuthor/@author.Id'>
&#8194;Edit&#8194;
</a>&#8194;
<a class="btn btn-warning" @onclick="() =>
OpenDialog(author.Id,author.LName)"
>
Delete</a>
</td>
</tr>
}
}
</tbody>
</table>
<div class="pagination">
<button class="btn btn-custom" @onclick=@(async ()=>
await NavigateToPage("previous"))>◀</button>
@for (int i = startPage; i <= endPage; i++)
{
var currentPage = i;
<button class="btn btn-custom pagebutton
@(currentPage==curPage?"btn-info":"")"
@onclick=@(async () =>
await refreshRecords(currentPage))>
@currentPage
</button>
}
<button class="btn btn-custom" @onclick=@(async ()=>
await NavigateToPage("next"))>▶</button>
</div>
}
@if (DialogIsOpen)
{
<Dialog Caption="Delete an author"
Message="@message"
OnClose="@OnDialogClose"
Type="Dialog.Category.DeleteNot">
</Dialog>
}
@code {
private string searchTerm;
private string SearchTerm
{
get { return searchTerm; }
set { searchTerm = value; FilterRecords(); }
}
List<Author> authors;
private int idAuthor;
private string message;
private bool DialogIsOpen = false;
#region Pagination int totalPages;
int totalRecords;
int curPage;
int pagerSize;
int pageSize;
int startPage;
int endPage;
string sortColumnName = "Id";
string sortDir = "ASC";
#endregion protected override async Task OnInitializedAsync()
{
//display total page buttons
pagerSize = 3;
pageSize = 5;
curPage = 1;
authors = await authorService.ListAll((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, searchTerm);
totalRecords = await authorService.Count(searchTerm);
totalPages = (int)Math.Ceiling
(totalRecords / (decimal)pageSize);
SetPagerSize("forward");
}
private void OpenDialog(int id, string title)
{
DialogIsOpen = true;
idAuthor = id;
message = "Do you want to delete the author \""
+ title + "\"?";
}
private async Task OnDialogClose(bool isOk)
{
if (isOk)
{
await authorService.Delete(idAuthor);
authors = await authorService.ListAll((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, searchTerm);
}
DialogIsOpen = false;
}
private bool isSortedAscending;
private string activeSortColumn;
private async Task<List<Author>>
SortRecords(string columnName, string dir)
{
return await authorService.ListAll((curPage - 1) *
pageSize, pageSize, columnName, dir, searchTerm);
}
private async Task SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
authors = await SortRecords(columnName, "ASC");
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
authors = await SortRecords(columnName, "DESC");
}
else
{
authors = await SortRecords(columnName, "ASC");
}
isSortedAscending = !isSortedAscending;
}
sortColumnName = columnName;
sortDir = isSortedAscending ? "ASC" : "DESC";
}
private string SetSortIcon(string columnName)
{
if (activeSortColumn != columnName)
{
return string.Empty;
}
if (isSortedAscending)
{
return "fa-sort-up";
}
else
{
return "fa-sort-down";
}
}
public async Task refreshRecords(int currentPage)
{
authors = await authorService.ListAll((currentPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, searchTerm);
curPage = currentPage;
this.StateHasChanged();
}
public void SetPagerSize(string direction)
{
if (direction == "forward" && endPage < totalPages)
{
startPage = endPage + 1;
if (endPage + pagerSize < totalPages)
{
endPage = startPage + pagerSize - 1;
}
else
{
endPage = totalPages;
}
this.StateHasChanged();
}
else if (direction == "back" && startPage > 1)
{
endPage = startPage - 1;
startPage = startPage - pagerSize;
}
else
{
startPage = 1;
endPage = totalPages;
}
}
public async Task NavigateToPage(string direction)
{
if (direction == "next")
{
if (curPage < totalPages)
{
if (curPage == endPage)
{
SetPagerSize("forward");
}
curPage += 1;
}
}
else if (direction == "previous")
{
if (curPage > 1)
{
if (curPage == startPage)
{
SetPagerSize("back");
}
curPage -= 1;
}
}
await refreshRecords(curPage);
}
public void FilterRecords()
{
endPage = 0;
this.OnInitializedAsync().Wait();
}
}

Yang dicetak tebal adalah kode yang dimodifikasi dan kode tambahan.

Implementasi Konfirmasi Update

Sebagai contoh, kita akan mengimplementasikan konfirmasi update data publisher (penerbit).

  • Pada jendela Solution Explorer, buka folder Pages.
  • Klik ganda EditPublisher.razor, lalu pilih submenu Open.
  • Lakukan modifikasi isi file EditPublisher.razor sehingga kode selengkapnya adalah sebagai berikut.

EditPublisher.razor

@page "/editPublisher/{id:int}"
@inject IPublisherService publisherService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h2>
Edit Publisher
</h2>
<form>
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<label for="Name" class="control-label">
Name
</label>
</td>
<td>
<input for="Name" class="form-control"
@bind="@publisher.Name" />
</td>
</tr>
<tr>
<td>
<label for="City" class="control-label">
City
</label>
</td>
<td>
<input for="City" class="form-control"
@bind="@publisher.City" />
</td>
</tr>
<tr>
<td>
<label for="Country" class="control-label">
Country
</label>
</td>
<td>
<input for="Country" class="form-control"
@bind="@publisher.Country" />
</td>
</tr>
<tr>
<td></td>
<td>
<br />
<button type="button" class="btn btn-primary"
@onclick="() => OpenDialog(publisher.Name)">
&#8195;Save&#8195;
</button>&#8194;
<button type="button" class="btn btn-warning"
@onclick="() => Cancel()">
&#8194;&#8201;Cancel&#8201;&#8194;
</button>
</td>
</tr>
</tbody>
</table>
</form>
@if (DialogIsOpen)
{
<Dialog Caption="Update a publisher"
Message="@message"
OnClose="@OnDialogClose"
Type="Dialog.Category.SaveNot">
</Dialog>
}
@code {[Parameter] public int id { get; set; }Publisher publisher = new Publisher();
private string message;
private bool DialogIsOpen = false;
protected override async Task OnInitializedAsync()
{
publisher = await publisherService.ReadByPk(id);
}
private void OpenDialog(string title)
{
DialogIsOpen = true;
message =
"Do you want to save the updates of the publisher \""
+ title + "\"?";
}
private async Task OnDialogClose(bool isOk)
{
if (isOk)
{
await publisherService.Update(publisher);
}
navigationManager.NavigateTo("/listPublisher");
DialogIsOpen = false;
}
void Cancel()
{
navigationManager.NavigateTo("/listPublisher");
}
}

Yang dicetak tebal adalah kode yang dimodifikasi dan kode tambahan.

Bagaimana Cara Kerjanya?

Sebagai contoh, berikut akan dibahas bagaimana konfirmasi penghapusan berproses.

  • Klik Authors untuk membuka halaman daftar pengarang.
  • Pada halaman daftar pengarang— ListAuthor.razor sebagai komponen induk— klik tombol Delete.
  1. Terjadi event onclick yang memicu metode OpenDialog(). Nilai DialogIsOpen pada baris 131 dan message pada baris 134 secara otomatis sama dengan nilai yang diproses oleh metode OpenDialog().
  2. Karena variabel DialogIsOpen bernilai true, komponen ListAuthor.razor menjalankan pernyataan baris 131 s.d 138, sehingga argumen (parameter aktual) memiliki nilai untuk dikirim ke komponen dialog Dialog.razor. Pernyataan OnClose=”@OnDialogClose” membuat event OnClose pada komponen anak (Dialog.razor) terikat ke metode OnDialogClose di komponen induk (ListAuthor.razor). Sedangkan pernyataan Type=”Dialog.Category.DeleteNot” berguna untuk menentukan jenis dialog.
  3. Membuka komponen dialog. Dialog.razor sebagai komponen anak menerima nilai argumen dari komponen induk.

Sampai di sini, ada dua pilihan bagi pengguna:

  • membatalkan penghapusan dengan mengklik tombol Don’t Delete atau tombol X.
  • menyetujui penghapusan dengan mengklik Delete.

Membatalkan penghapusan

Pengguna mengklik tombol Don’t Delete atau tombol X.

  1. Terjadi event onclick yang memicu metode Cancel().
  2. Event OnClose sebagai EventCallback mengirimkan argumen yang bernilai false ke ListAuthor.razor.

Menyetujui penghapusan

Pengguna mengklik tombol Delete.

  1. Terjadi event onclick yang memicu metode Ok().
  2. Event OnClose sebagai EventCallback mengirimkan argumen yang bernilai true ke ListAuthor.razor.

Event OnClose pada Dialog.razor memicu metode OnDialogClose(bool isOk) di ListAuthor.razor yang akan menghapus data jika dan hanya jika argumennya bernilai true.

Selain untuk konfirmasi penghapusan dan update, komponen Dialog.razor dapat juga dipergunakan untuk keperluan lain, misalnya untuk menampilkan pesan kesalahan atau sekedar menampilkan informasi.

Mungkin ada cara yang lebih baik untuk membuat komponen dialog modal yang dapat digunakan kembali dan menerapkannya pada konfirmasi penghapusan dan/atau konfirmasi pembaruan. Tolong beri tahu saya jika Anda mengetahuinya.

Mungkin ada cara yang lebih baik untuk mengimplementasikan konfirmasi penghapusan dan konfirmasi update. Tolong beri tahu saya jika Anda mengetahuinya.

Terima kasih sudah membaca. Semoga berfaedah.

--

--

M. Ramadhan
Telematika

I’m a database designer and developer, childhood in Menggala, living in Palembang.