Proyek Blazor Server #6: Membuat Komponen Dialog Reusable
Diimplementasikan untuk konfirmasi penghapusan dan update (pembaruan)
Daftar Isi
⦁ Pendahuluan
⦁ Membuat Komponen Dialog
⦁ Implementasi Konfirmasi Penghapusan
⦁ Implementasi Konfirmasi Update
⦁ Bagaimana Cara Kerjanya?
⦁ Referensi
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 kananBookApp
. - Pilih submenu:
Add
|New Folder
.
- Ketik
Components
sebagai nama foldernya.
(3) Membuat file komponen Dialog
.
- Pada jendela
Solution Explorer
, klik kanan folderComponents
. - Pilih submenu
Add
|Razor Component...
- Klik
Razor Component
. - Di kotak teks
Name
, ketikDialog.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">×</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 motodeOk()
.false
bila pengguna mengklik tombol kedua yang kemudian memicu motodeCancel()
.
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 folderPages
. - Klik kanan
ListAuthor.razor
, lalu pilih submenuOpen
. - Lakukan modifikasi isi file
ListAuthor.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'>
 Edit 
</a> 
<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 folderPages
. - Klik ganda
EditPublisher.razor
, lalu pilih submenuOpen
. - 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)">
 Save 
</button> 
<button type="button" class="btn btn-warning"
@onclick="() => Cancel()">
  Cancel  
</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 tombolDelete
.
- Terjadi event
onclick
yang memicu metodeOpenDialog()
. NilaiDialogIsOpen
pada baris 131 danmessage
pada baris 134 secara otomatis sama dengan nilai yang diproses oleh metodeOpenDialog()
. - Karena variabel
DialogIsOpen
bernilaitrue
, komponenListAuthor.razor
menjalankan pernyataan baris 131 s.d 138, sehingga argumen (parameter aktual) memiliki nilai untuk dikirim ke komponen dialogDialog.razor
. PernyataanOnClose=”@OnDialogClose”
membuat eventOnClose
pada komponen anak (Dialog.razor
) terikat ke metodeOnDialogClose
di komponen induk (ListAuthor.razor
). Sedangkan pernyataanType=”Dialog.Category.DeleteNot”
berguna untuk menentukan jenis dialog. - 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 tombolX
. - menyetujui penghapusan dengan mengklik
Delete
.
Membatalkan penghapusan
Pengguna mengklik tombol Don’t Delete
atau tombol X
.
- Terjadi event
onclick
yang memicu metodeCancel()
. - Event
OnClose
sebagaiEventCallback
mengirimkan argumen yang bernilaifalse
keListAuthor.razor
.
Menyetujui penghapusan
Pengguna mengklik tombol Delete
.
- Terjadi event
onclick
yang memicu metodeOk()
. - Event
OnClose
sebagaiEventCallback
mengirimkan argumen yang bernilaitrue
keListAuthor.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.