Proyek Blazor Server #7: Panduan Praktis Membuat Halaman Master-Detail
Implementasi operasi CRUD untuk relationship 1:N & M:N dan navigasi dinamis menggunakan rute berparameter
Daftar isi
- Tinjauan Sekilas
- Implementasi Halaman Master-Detail
(1) Membuka proyek BookApp
(2) Membuat basis data BookDB
(3) Menambahkan antarmuka dan metode
(4) Membuat file komponenListBookPerPub.razor
(5) Membuat file komponenListBookPerAuthor.razor
(6) Memodifikasi rute dan navigasinya - Bagaimana Cara Kerjanya?
- Penutup
- Daftar Pustaka
Ini adalah tulisan ketujuh dari serangkaian artikel yang membahas Proyek Server Blazor :
(1) CRUD untuk tabel tunggal menggunakan Dapper.
(2) Implementasi drop-down list pada operasi CRUD.
(3) Penerapan checkbox list pada operasi CRUD.
(4) Memahami perutean dan navigasi URL.
(5) Membuat dan menggunakan tata letak halaman.
(6) Membuat komponen dialog modal reusable.
(7) Panduan praktis untuk membuat halaman master-detail.
Artikel ditujukan bagi siapa pun yang ingin mempelajari cara membuat aplikasi Blazor Server dengan pendekatan praktis melalui contoh proyek. Hal ini akan lebih mudah jika Anda memiliki sedikit pengalaman dengan C#, HTML, CSS, dan SQL.
Semua pembahasan berikut berdasarkan kode sumber di artikel sebelumnya, Proyek Blazor Server #6 . Silahkan download source codenya melalui link di bawah ini.
Tinjauan Sekilas
CRUD (Create, Read, Update, Delete) adalah fitur yang harus ada pada aplikasi yang menggunakan basis data. Pada artikel sebelumnya, kita telah membahas bagaimana mengimplementasikan operasi CRUD yang melibatkan:
(1) hanya satu tabel
(2) dua tabel dengan relationship N:1
(3) tiga tabel dengan relationship M:N.
Aplikasi yang telah dibangun menggunakan basis data berdasarkan ERD berikut.
Kali ini kita akan membahas cara membuat dua halaman master-detail:
- Daftar buku per penerbit sebagai implementasi dari relationship 1:N, Publisher-publishes-Book .
- Daftar buku per penulis sebagai implementasi dari relationship M:N, Author-writes-Book.
Daftar buku per penerbit
- Secara default, opsi dalam drop-down list adalah
[All]
; semua buku yang ada ditampilkan dalam daftar. - Anda dapat menampilkan daftar buku dari penerbit tertentu dengan memilih penerbit dari drop-down list.
Daftar buku per penulis
Versi pertama
Kedua buku dalam daftar di atas menampilkan hanya seorang pengarang, padahal keduanya ditulis oleh dua orang penulis. Relationship M:N tidak terlihat pada gambar di atas.
Versi kedua
Versi kedua menjelaskan hubungan M:N dengan lebih jelas. Seorang penulis dapat menulis banyak buku; sebaliknya, satu buku dapat ditulis oleh banyak penulis. Jadi kita akan membuat yang ini.
Implementasi Halaman Master-Detail
(1) Membuka proyek BookApp
- Klik kanan file
BookApp.sln
, yaitu salah satu file yang telah Anda unduh sebelumnya.
- Klik menu
Open
untuk membuka aplikasiBookApp
.
(2) Membuat basis data BookDB
(a) Membuka jendela SQL Server Object Explorer
- Klik menu:
View
|SQL Server Object Explorer
- Lihat jendela
SQL Server Object Explorer
. Jika basis dataBookDB
sudah ada, lanjut ke langkah (3). Jika belum ada, lakukan langkah berikut.
(b) Membuka dan mengeksekusi BookDB.sql
file
- Di jendela
Solution Explorer
, klikBookDB.sql
untuk membuka file skrip SQL. - Klik tombol
▶
untuk menjalankan seluruh skrip SQL dalam fileBookDB.sql
.
- Pilih server Anda, klik tombol
Connect
.
- Di jendela
SQL Server Object Explorer
, klik kananDatabases
, lalu klikRefresh
.
(3) Menambahkan antarmuka dan metode
- Tambahkan antarmuka berikut ke dalam file
IBookService.cs
.
Task<List<BookAuPub>> ListPerPub(int skip,
int take,
string orderBy,
string direction,
int idPub);
Task<List<BookAuPub>> ListPerAuthor(int skip,
int take,
string orderBy,
string direction,
int idAuthor);
Task<int> CountBookPerPub(int idPub);Task<int>CountBookPerAuthor(int idAuthor);
- Tambahkan kode berikut ke dalam file
BookService.cs
.
public Task<List<BookAuPub>> ListPerPub(int skip, int take,
string orderBy, string direction = "DESC", int idPub = 0)
{
var books = Task.FromResult(_dapperService.
GetAll<BookAuPub>($"SELECT B.*, FName + ' ' + LName " +
$"AuthorName, P.Name PubName FROM Book B LEFT OUTER JOIN" +
$" Publisher P ON B.PubId=P.Id LEFT OUTER JOIN " +
$"BookAuthor BA ON B.ISBN = BA.ISBN LEFT OUTER JOIN " +
$"Author A ON BA.AuthorId = A.Id " +
$"WHERE PubId = {idPub} ORDER BY {orderBy} " +
$"{direction} OFFSET {skip} ROWS FETCH NEXT {take} " +
$"ROWS ONLY;", null, commandType: CommandType.Text));
return books;
}public Task<List<BookAuPub>> ListPerAuthor(int skip, int take,
string orderBy, string direction = "DESC", int idAuthor = 0)
{
var books = Task.FromResult(_dapperService.
GetAll<BookAuPub>($"SELECT B.*, FName + ' ' + LName " +
$"AuthorName, P.Name PubName FROM Book B LEFT OUTER JOIN" +
$" Publisher P ON B.PubId=P.Id LEFT OUTER JOIN " +
$"BookAuthor BA ON B.ISBN = BA.ISBN LEFT OUTER JOIN " +
$"Author A ON BA.AuthorId = A.Id WHERE B.ISBN IN (" +
$"SELECT ISBN From BookAuthor WHERE AuthorId={idAuthor}" +
$") ORDER BY {orderBy} {direction} OFFSET {skip} " +
$"ROWS FETCH NEXT {take} ROWS ONLY;", null,
commandType: CommandType.Text));
return books;
}public Task<int> CountBookPerPub(int idPub)
{
var totBook = Task.FromResult(_dapperService.Get<int>
($"select COUNT(*) from [Book] " +
"WHERE PubId = {idPub}", null,
commandType: CommandType.Text));
return totBook;
}public Task<int> CountBookPerAuthor(int idAuthor)
{
var totBook = Task.FromResult(_dapperService.Get<int>
($"SELECT COUNT(*) FROM Book B INNER JOIN BookAuthor BA" +
$" ON B.ISBN = BA.ISBN WHERE AuthorId = {idAuthor}", null,
commandType: CommandType.Text));
return totBook;
}
- Metode
ListPerPub()
memberikan daftar buku yang diterbitkan oleh sebuah penerbit. - Metode
ListPerAuthor()
menghasilkan daftar buku yang ditulis oleh seorang penulis. - Metode
CountBookPerPub()
menghitung jumlah buku yang diterbitkan oleh suatu penerbit. - Metode
CountBookPerAuthor()
menghitung jumlah buku yang ditulis oleh seorang penulis.
(4) Membuat file komponen ListBookPerPub.razor
- Di dalam jendela
Solution Explorer
, klik kanan folderPage
. - Pilih submenu:
Add
|Razor Component...
- Klik
Razor Component
. - Di dalam kotak teks
Name
, ketikkanListBookPerPub.razor
sebagai nama file komponen. - Klik tombol
Add
.
- Klik
ListBookPerPub.razor
untuk membuka file.
Berdasarkan Gambar 3, bagian master adalah penerbit, dan detailnya adalah daftar buku. Silakan salin kode di bawah ini dan tempel ke file ListBookPerPub.razor
.
@page "/listBookPerPub"
@inject IBookService bookService
@inject IPublisherService publisherService<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"><h3>
Booklist per publisher
</h3><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><form>
<label for="Publisher">Publisher:</label>
<select for="Publisher" @bind="IdPubFilter"
@bind:event="onchange">
<option value=@All>[ALL]</option>
@foreach (var publisher in publishers)
{
<option value="@publisher.Id">@publisher.Name</option>
}
</select> 
<label for="City">City:</label>
<input for="City" @bind="@publisher.City" disabled/> 
<label for="Country">Country:</label>
<input for="Country" @bind="@publisher.Country" disabled/>
</form>
<br />@if (books == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th class="sort-th"
@onclick="@(() => SortTable("ISBN"))">
I S B N
<span class="fa @(SetSortIcon("ISBN"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("Title"))">
T i t l e
<span class="fa @(SetSortIcon("Title"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("AuthorName"))">
Author
<span class="fa @(SetSortIcon("AuthorName"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PubYear"))">
Pub.<br />
Year
<span class="fa @(SetSortIcon("PubYear"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PurchDate"))">
Purchase<br />Date
<span class="fa @(SetSortIcon("PurchDate"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PubName"))">
Publisher
<span class="fa @(SetSortIcon("PubName"))"></span>
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@if (books == null || books.Count == 0)
{
<tr>
<td colspan="3">
No Records to display
</td>
</tr>
}
else
{
long prevISBN = 0;
foreach (var book in books)
{
if (@book.ISBN != prevISBN)
{
<tr>
<td>
<hr style="padding:0px; margin:0px;">
@book.ISBN
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.Title
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.AuthorName
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PubYear
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PurchDate.ToShortDateString()
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PubName
</td>
<td>
<hr style="padding:0px; margin:0px;">
<a class="btn btn-primary"
href='/editBook/@name/@book.ISBN'>
 Edit 
</a> 
<a class="btn btn-warning" @onclick="() =>
OpenDialog((long)book.ISBN,book.Title)">
Delete</a>
</td>
</tr>
}
else
{
<tr>
<td></td>
<td></td>
<td>
<hr style="padding:0px; margin:0px;">
@book.AuthorName
</td>
</tr>
}
prevISBN = (long)@book.ISBN;
}
}
</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> 
<button class="btn btn-primary"
onclick="window.location.href='/addBook/@name'">
Add new book
</button>
</div>
}@if (DialogIsOpen)
{
<Dialog Caption="Delete a book"
Message="@message"
OnClose="@OnDialogClose"
Type="Dialog.Category.DeleteNot">
</Dialog>
}@code {
private const string name = "listBookPerPub";
Publisher publisher = new Publisher();
List<BookAuPub> books;
List<Publisher> publishers = new List<Publisher>();
private long idBook;
private string message;
private bool DialogIsOpen = false; const int All = -1;
private int idPubFilter = All;
private int IdPubFilter
{
get { return idPubFilter; }
set { idPubFilter = value; this.InitializedAsync().Wait(); }
} #region Pagination int totalPages;
int totalRecords;
int curPage;
int pagerSize;
int pageSize;
int startPage;
int endPage;
string sortColumnName = "PurchDate";
string sortDir = "DESC"; #endregion protected override async Task OnInitializedAsync()
{
publishers = await publisherService.FetchAll();
await InitializedAsync();
} protected async Task InitializedAsync()
{
//display total page buttons
pagerSize = 3;
pageSize = 5;
curPage = 1;
endPage = 0;
if (idPubFilter == All)
{
books = await bookService.ListAll((curPage - 1) * pageSize,
pageSize, sortColumnName, sortDir, "");
totalRecords = await bookService.Count("");
publisher.City = "";
publisher.Country = "";
}
else
{
publisher = await publisherService.ReadByPk(idPubFilter);
books = await bookService.ListPerPub((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir,idPubFilter);
totalRecords = await bookService.CountResult(idPubFilter);
}
totalPages = (int)Math.Ceiling
(totalRecords / (decimal)pageSize);
SetPagerSize("forward");
} private void OpenDialog(long isbn, string title)
{
DialogIsOpen = true;
idBook = isbn;
message = "Do you want to delete the book \"" + title + "\"?";
} private async Task OnDialogClose(bool isOk)
{
if (isOk)
{
await bookService.Delete(idBook);
if (idPubFilter == All)
{
books = await bookService.ListAll((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, "");
}
else
{
books = await bookService.ListPerPub((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir,
idPubFilter);
}
}
DialogIsOpen = false;
} private bool isSortedAscending;
private string activeSortColumn; private async Task<List<BookAuPub>> SortRecords
(string columnName, string dir)
{
if (idPubFilter == All)
{
return await bookService.ListAll((curPage - 1) *
pageSize, pageSize, columnName, dir, "");
}
else
{
return await bookService.ListPerPub((curPage - 1) *
pageSize, pageSize, columnName, dir, idPubFilter);
}
} private async Task SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
books = await SortRecords(columnName, "ASC");
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
books = await SortRecords(columnName, "DESC");
}
else
{
books = 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)
{
if (idPubFilter == All)
{
books = await bookService.ListAll((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, "");
}
else
{
books = await bookService.ListPerPub((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, idPubFilter);
}
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);
}
}
(5) Membuat file komponenListBookPerAuthor.razor
- Sama seperti pada langkah (4), buat file komponen bernama
ListBookPerAuthor.razor
. - Berdasarkan Gambar 5 di atas, masternya adalah penulis, dan detailnya adalah daftar buku. Silahkan salin kode di bawah ini dan tempel ke dalam file
ListBookPerAuthor.razor
.
@page "/listBookPerAuthor"
@inject IBookService bookService
@inject IAuthorService authorService<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"><h3>
Booklist per Author
</h3><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><form>
<label for="Author">Author:</label>
<select for="Author" @bind="IdAuthorFilter"
@bind:event="onchange">
<option value=@All>[ALL]</option>
@foreach (var author in authors)
{
<option value="@author.Id">
@author.FName @author.LName
</option>
}
</select> 
<label for="Phone">Phone:</label>
<input for="Phone" @bind="@author.Phone" disabled />
</form>
<br />@if (books == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th class="sort-th"
@onclick="@(() => SortTable("ISBN"))">
I S B N
<span class="fa @(SetSortIcon("ISBN"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("Title"))">
T i t l e
<span class="fa @(SetSortIcon("Title"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("AuthorName"))">
Author
<span class="fa @(SetSortIcon("AuthorName"))">
</span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PubYear"))">
Pub.<br />Year
<span class="fa @(SetSortIcon("PubYear"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PurchDate"))">
Purchase<br />Date<span class=
"fa @(SetSortIcon("PurchDate"))"></span>
</th>
<th class="sort-th"
@onclick="@(() => SortTable("PubName"))">
Publisher<span class=
"fa @(SetSortIcon("PubName"))"></span>
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@if (books == null || books.Count == 0)
{
<tr>
<td colspan="3">
No Records to display
</td>
</tr>
}
else
{
long prevISBN = 0;
foreach (var book in books)
{
if (@book.ISBN != prevISBN)
{
<tr>
<td>
<hr style="padding:0px; margin:0px;">
@book.ISBN
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.Title
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.AuthorName
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PubYear
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PurchDate.ToShortDateString()
</td>
<td>
<hr style="padding:0px; margin:0px;">
@book.PubName
</td>
<td>
<hr style="padding:0px; margin:0px;">
<a class="btn btn-primary"
href='/editBook/@name/@book.ISBN'>
 Edit 
</a> 
<a class="btn btn-warning" @onclick="() =>
OpenDialog((long)book.ISBN,book.Title)">
Delete
</a>
</td>
</tr>
}
else
{
<tr>
<td></td>
<td></td>
<td>
<hr style="padding:0px; margin:0px;">
@book.AuthorName
</td>
</tr>
}
prevISBN = (long)@book.ISBN;
}
}
</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> 
<button class="btn btn-primary"
onclick="window.location.href='/addBook/@name'">
Add new book
</button>
</div>
}@if (DialogIsOpen)
{
<Dialog Caption="Delete a book"
Message="@message"
OnClose="@OnDialogClose"
Type="Dialog.Category.DeleteNot">
</Dialog>
}@code {
private const string name = "listBookPerAuthor";
Author author = new Author();
List<BookAuPub> books;
List<Author> authors = new List<Author>();
private long idBook;
private string message;
private bool DialogIsOpen = false; const int All = -1;
private int idAuthorFilter = All;
private int IdAuthorFilter
{
get { return idAuthorFilter; }
set { idAuthorFilter = value; this.InitializedAsync().Wait(); }
} #region Pagination int totalPages;
int totalRecords;
int curPage;
int pagerSize;
int pageSize;
int startPage;
int endPage;
string sortColumnName = "PurchDate";
string sortDir = "DESC"; #endregion protected override async Task OnInitializedAsync()
{
authors = await authorService.FetchAll();
await InitializedAsync();
} protected async Task InitializedAsync()
{
//display total page buttons
pagerSize = 3;
pageSize = 5;
curPage = 1;
endPage = 0;
if (idAuthorFilter == All)
{
books = await bookService.ListAll((curPage - 1) * pageSize,
pageSize, sortColumnName, sortDir, "");
totalRecords = await bookService.Count("");
author.Phone = "";
}
else
{
author = await authorService.ReadByPk(idAuthorFilter);
books = await bookService.ListPerAuthor((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir,
idAuthorFilter);
totalRecords = await bookService.CountResult
(idAuthorFilter);
}
totalPages = (int)Math.Ceiling
(totalRecords / (decimal)pageSize);
SetPagerSize("forward");
} private void OpenDialog(long isbn, string title)
{
DialogIsOpen = true;
idBook = isbn;
message = "Do you want to delete the book \"" + title + "\"?";
} private async Task OnDialogClose(bool isOk)
{
if (isOk)
{
await bookService.Delete(idBook);
if (idAuthorFilter == All)
{
books = await bookService.ListAll((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir, "");
}
else
{
books = await bookService.ListPerAuthor((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir,
idAuthorFilter);
}
}
DialogIsOpen = false;
} private bool isSortedAscending;
private string activeSortColumn; private async Task<List<BookAuPub>> SortRecords
(string columnName, string dir)
{
if (idAuthorFilter == All)
{
return await bookService.ListAll((curPage - 1) *
pageSize, pageSize, columnName, dir, "");
}
else
{
return await bookService.ListPerAuthor((curPage - 1) *
pageSize, pageSize, columnName, dir, idAuthorFilter);
}
} private async Task SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
books = await SortRecords(columnName, "ASC");
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
books = await SortRecords(columnName, "DESC");
}
else
{
books = 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)
{
if (idAuthorFilter == All)
{
books = await bookService.ListAll((curPage - 1) * pageSize,
pageSize, sortColumnName, sortDir, "");
}
else
{
books = await bookService.ListPerAuthor((curPage - 1) *
pageSize, pageSize, sortColumnName, sortDir,
idAuthorFilter);
}
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);
}
}
(6) Memodifikasi rute dan navigasinya
Dalam proyek sebelumnya, file AddBook.razor
dan EditBook.razor
hanya dipanggil oleh ListBook.razor
. Lihat gambar berikut.
Navigasi menggunakan rute statis, seperti yang ditunjukkan pada gambar berikut.
Dalam proyek ini, halaman AddBook.razor
dan EditBook.razor
— dipanggil oleh ListBook.razor
, ListBookPerPub.razor
, dan ListBookPerAuthor.razor
.
Jadi kita harus memodifikasi rute dan navigasi di kedua file komponen, AddBook.razor
dan EditBook.razor
. Kali ini, seperti yang ditunjukkan pada kode di bawah ini, kita menggunakan rute berparameter untuk bernavigasi secara dinamis.
AddBook.razor
@page "/addBook/{PrevPage}"
@inject IBookService bookService
@inject IPublisherService publisherService
@inject IBookAuthorService bookAuthorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h3>
Add Book
</h3><form>
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td><label for="ISBN" class="control-label">ISBN</label>
</td>
<td><input for="ISBN" class="form-control"
@bind="@book.ISBN" /></td>
</tr>
<tr>
<td><label for="Title" class="control-label">
Title</label></td>
<td><input for="Title" class="form-control"
@bind="@book.Title" /></td>
</tr>
@if (@hasAdded)
{
<tr>
<td><label for="Authors" class="control-label">
Authors</label></td>
<td>
<label style="width: 95px;">
<u><i><b>Sequence</b></i></u>
</label>
<label><u><i><b>Full Name</b></i></u></label>
<br />
@foreach (var bookAuthorName in bookAuthorNames)
{
<input type="number"
@bind="@bookAuthorName.AuthorOrd"
@bind:event="oninput"
style="width: 75px;" min="0" max="5" />
@if (@bookAuthorName.ISBN > 0)
{
<input name="AreChecked" type="checkbox"
value="@bookAuthorName.AuthorId" checked
@onchange="eventArgs => { CheckChanged
(bookAuthorName, eventArgs.Value);}" />
}
else
{
<input name="AreChecked" type="checkbox"
value="@bookAuthorName.AuthorId"
@onchange="eventArgs => { CheckChanged
(bookAuthorName, eventArgs.Value);}" />
}
<label for="AuthorName" style="width: 150px;">
@bookAuthorName.AuthorName
</label><br />
}
</td>
<td>
 
<a class="btn btn-primary"
href='/addAuthor/@book.ISBN'>
 Add author 
</a><br />
</td>
</tr>
}
<tr>
<td><label for="PubYear" class="control-label">
Publication Year</label></td>
<td><input for="PubYear" class="form-control"
@bind="@book.PubYear" /></td>
</tr>
<tr>
<td><label for="PurchDate" class="control-label">
Purchase Date</label></td>
<td><input type="date" class="form-control"
@bind="@book.PurchDate" /></td>
</tr>
<tr>
<td><label for="Publisher" class="control-label">
Publisher</label></td>
<td>
<select for="Publisher" class="form-control"
@bind="@book.PubId">
<option value=0 disabled selected hidden>
[Select Publisher]</option>
@foreach (var publisher in publishers)
{
<option value="@publisher.Id">
@publisher.Name</option>
}
</select>
</td>
<td>
 
<a class="btn btn-primary"
href='/addPublisher/@book.ISBN'>Add publisher</a>
</td>
</tr>
<tr>
<td></td>
<td>
<br />
<button type="button" class="btn btn-primary"
@onclick="() => CreateBook()">
 Save 
</button> 
<button type="button" class="btn btn-warning"
@onclick="() => Cancel()">
  Cancel  
</button>
</td>
</tr>
</tbody>
</table>
</form>@code { [Parameter] public string PrevPage { get; set; }
Book book = new Book();
BookAuthorName bookAuthorName = new BookAuthorName();
List<Publisher> publishers = new List<Publisher>();
List<BookAuthorName>bookAuthorNames = new List<BookAuthorName>();
List<Book> books = new List<Book>();
bool hasAdded = false; protected override async Task OnInitializedAsync()
{
book.PurchDate = DateTime.Now;
publishers = await publisherService.FetchAll();
} protected async Task CreateBook()
{
if (hasAdded)
{
navigationManager.NavigateTo(PrevPage);
}
else
{
await bookService.Create(book);
bookAuthorNames = await bookAuthorService.FetchAll(0);
hasAdded = !hasAdded;
}
} protected async Task CheckChanged(BookAuthorName bookAuthorName,
object checkValue)
{
long isbn = 0;
if (book.ISBN > isbn)
{
isbn = (long)book.ISBN;
bookAuthorNames = await bookAuthorService.FetchAll(isbn);
if ((bool)checkValue)
{
// insert
bookAuthorName.ISBN = isbn;
await bookAuthorService.Create(bookAuthorName);
}
else
{
//delete
bookAuthorName.AuthorOrd = 0;
await bookAuthorService.Delete
(isbn, bookAuthorName.AuthorId);
}
bookAuthorNames = await bookAuthorService.FetchAll(isbn);
}
} void Cancel()
{
navigationManager.NavigateTo(PrevPage);
}
}
EditBook.razor
@page "/editBook/{PrevPage}/{Isbn:long}"
@inject IBookService bookService
@inject IPublisherService publisherService
@inject IBookAuthorService bookAuthorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager;<h3>
Edit Book
</h3>
<form>
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td>
<label for="ISBN" class="control-label">
ISBN
</label>
</td>
<td>
<input for="ISBN" class="form-control"
@bind="@book.ISBN" />
</td>
</tr>
<tr>
<td>
<label for="Title" class="control-label">
Title
</label>
</td>
<td>
<input for="Title" class="form-control"
@bind="@book.Title" />
</td>
</tr>
<tr>
<td>
<label for="Authors" class="control-label">
Authors
</label>
</td>
<td>
<label style="width: 95px;">
<u><i><b>Sequence</b></i></u>
</label>
<label><u><i><b>Full Name</b></i></u></label><br />
@foreach (var bookAuthorName in bookAuthorNames)
{
<input type="number"
@bind="@bookAuthorName.AuthorOrd"
@bind:event="oninput"
style="width: 75px;" min="0" max="5" />
@if (@bookAuthorName.ISBN > 0)
{
<input name="AreChecked" type="checkbox"
value="@bookAuthorName.AuthorId" checked
@onchange="eventArgs => { CheckChanged
(bookAuthorName, eventArgs.Value);}" />
}
else
{
<input name="AreChecked" type="checkbox"
value="@bookAuthorName.AuthorId"
@onchange="eventArgs => { CheckChanged
(bookAuthorName, eventArgs.Value);}" />
}
<label for="AuthorName" style="width: 150px;">
@bookAuthorName.AuthorName
</label><br />
}
</td>
<td>
 
<a class="btn btn-primary"
href='/addAuthor/@book.ISBN'>
 Add new author 
</a><br />
</td>
</tr>
<tr>
<td>
<label for="PubYear" class="control-label">
Publication Year
</label>
</td>
<td>
<input for="PubYear" class="form-control"
@bind="@book.PubYear" />
</td>
</tr>
<tr>
<td>
<label for="PurchDate" class="control-label">
Purchase Date
</label>
</td>
<td>
<input type="date" class="form-control"
@bind="@book.PurchDate" />
</td>
</tr>
<tr>
<td>
<label for="Publisher" class="control-label">
Publisher
</label>
</td>
<td>
<select for="Publisher" class="form-control"
@bind="@book.PubId">
@foreach (var publisher in publishers)
{
<option value="@publisher.Id">
@publisher.Name
</option>
}
</select>
</td>
<td>
 
<a class="btn btn-primary"
href='/addPublisher/@book.ISBN'>Add new publisher
</a>
</td>
</tr>
<tr>
<td></td>
<td>
<br />
<button type="button" class="btn btn-primary"
@onclick="() => OpenDialog(book.Title)">
 Save 
</button> 
<button type="button" class="btn btn-warning"
@onclick="() => Cancel()">
  Cancel  
</button>
</td>
</tr>
</tbody>
</table>
</form>
@if (DialogIsOpen)
{
<Dialog
Caption="Update a book"
Message="@message"
OnClose="@OnDialogClose"
Type="Dialog.Category.SaveNot">
</Dialog>
}@code { [Parameter] public string PrevPage { get; set; }
[Parameter] public long Isbn { get; set; } Book book = new Book();
BookAuthorName bookAuthorName = new BookAuthorName();
List<Publisher> publishers = new List<Publisher>();
List<BookAuthorName> bookAuthorNames=new List<BookAuthorName>();
private string message;
private bool DialogIsOpen = false; protected override async Task OnInitializedAsync()
{
book = await bookService.ReadByPk(Isbn);
bookAuthorNames = await bookAuthorService.FetchAll(Isbn);
publishers = await publisherService.FetchAll();
} protected async Task CheckChanged(BookAuthorName bookAuthorName,
object checkValue)
{
if ((bool)checkValue)
{
// insert
bookAuthorName.ISBN = Isbn;
await bookAuthorService.Create(bookAuthorName);
}
else
{
//delete
bookAuthorName.AuthorOrd = null;
await bookAuthorService.Delete
(Isbn, bookAuthorName.AuthorId);
}
bookAuthorNames = await bookAuthorService.FetchAll(Isbn);
} private void OpenDialog(string title)
{
DialogIsOpen = true;
message = "Do you want to save the updates of the book \"" +
title + "\"?";
} private async Task OnDialogClose(bool isOk)
{
if (isOk)
{
await bookService.Update(book,Isbn);
navigationManager.NavigateTo(PrevPage);
}
DialogIsOpen = false;
} void Cancel()
{
navigationManager.NavigateTo(PrevPage);
}
}
Kode yang dimodifikasi dicetak tebal.
Bagaimana Cara Kerjanya?
Daftar buku per penerbit
- Klik menu
Book/Publisher
untuk membuka halamanListBookPerBook.razor
. - Secara default, opsi dalam drop-down list adalah
[All]
; semua buku yang ada ditampilkan dalam daftar.
(1) Membuka halaman
Halaman dibuka melalui komponen NavMenu.razor
menggunakan rute /listBookPerPub
.
(2) Membuat drop-down list dan mengatur opsi default ke [All]
- Ketika halaman
ListBookPerPub.razor
dibuka, aplikasi memanggil metodeOnInitializedAsync()
, mengeksekusi skrip SQL dalam metodeFetchAll()
dan menyimpan hasilnya dalam variabelpublishers
. - Melalui pengikatan data satu arah, drop-down list di blok HTML secara otomatis berisi data sesuai dengan hasil pemrosesan di blok kode.
- Menyetel opsi default ke
[All]
diperoleh melalui pernyataan pada baris 201 dalam fileListBookPerBook.razor
:private int idPubFilter = All;
(3) Menampilkan semua buku yang ada
Secara default, opsi dalam drop-down list adalah [All]
; semua buku yang ada ditampilkan dalam daftar.
- Aplikasi memanggil metode
InitializedAsync()
dan mengeksekusi skrip SQL dalam metodeListAll()
dalam fileBookService.cs
. - Melalui pengikatan data satu arah, blok HTML secara otomatis menampilkan detail daftar buku (jika ada) sesuai dengan hasil pemrosesan blok kode. Jika tidak, pesan
No Records to display
ditampilkan.
(4) Menampilkan daftar buku penerbit
- Pilih sebuah penerbit dari drop-down list untuk menampilkan daftar bukunya.
- Memilih penerbit dari drop-down list akan memunculkan event
onchange
yang memanggil propertiIdPubFilter
. - Properti
IdPubFilter
menyetel fieldidPubFilter
dan memanggil metodeInitalizedAsync
yang mengeksekusi skrip SQL dalam metodeReadByPk
danListPerPub
. - Melalui pengikatan data satu arah, blok HTML menampilkan kota dan negara penerbit yang dipilih. Secara otomatis menampilkan detail daftar buku (jika ada) sesuai dengan hasil pemrosesan di blok kode. Jika tidak, muncul pesan
No Records to display
.
Daftar buku per penulis
Cara kerjanya mirip dengan daftar buku per penerbit. Di bagian master, penerbit digantikan oleh penulis. Di bagian detail buku ini, ada sedikit perbedaan dalam skrip SQL. Dalam daftar buku per penulis , skrip SQL tidak hanya menggunakan outer join tetapi juga subquery.
Penutup
Dalam artikel ini, telah dibahas bagaimana cara membuat dan mengimplementasikan halaman master-detail.
- Daftar buku per penerbit sebagai implementasi relationhip 1:N. Bagian master menampilkan penerbit, dan bagian detail berisikan daftar bukunya.
- Daftar buku per penulis sebagai implementasi relationhip M:N. Bagian master menampilkan penulis, dan bagian detail berisikan daftar bukunya.
Selain itu, telah dijelaskan pula bagaiman menerapkan navigasi dinamis menggunakan rute berparameter.
Semoga berfaedah.