
Pendahuluan
Halo rekan!
Hari ini saya ingin berbagi dengan Anda pengalaman saya dalam mengembangkan arsitektur Model Tampilan sebagai bagian dari pengembangan aplikasi web ASP.NET menggunakan mesin template Razor .
Implementasi teknis yang dijelaskan dalam artikel ini cocok untuk semua versi ASP saat ini. NET ( MVC 5 , Core , dll). Artikel itu sendiri ditujukan untuk pembaca yang, setidaknya, sudah memiliki pengalaman bekerja dengan tumpukan ini. Perlu juga dicatat bahwa dalam kerangka ini, kami tidak mempertimbangkan kegunaan Model Tampilan dan aplikasi hipotetisnya (diasumsikan bahwa pembaca sudah terbiasa dengan hal-hal ini), kami langsung membahas implementasinya.
Tantangan
Untuk asimilasi material yang nyaman dan rasional, saya mengusulkan untuk segera mempertimbangkan tugas yang secara alami akan membawa kita pada masalah potensial dan solusi optimalnya.
Ini adalah masalah penambahan banal, katakanlah, mobil baru ke katalog kendaraan tertentu. Agar tidak menyulitkan tugas abstrak, detail dari aspek yang tersisa akan sengaja terlewatkan. Akan terlihat bahwa tugas dasar, bagaimanapun, adalah mencoba untuk melakukan semuanya dengan fokus pada penskalaan lebih lanjut dari sistem (khususnya, memperluas model relatif terhadap jumlah properti dan komponen definisi lainnya) sehingga nantinya akan senyaman mungkin untuk bekerja.
Implementasi
Biarkan model terlihat sebagai berikut (demi kesederhanaan, hal-hal seperti properti navigasi dan sebagainya tidak diberikan):
class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } }
Tentu saja, TransportTypeId adalah kunci asing ke objek tipe TransportType :
class TransportType { public int Id { get; set; } public string Name { get; set; } }
Untuk koneksi antara frontend dan backend kita akan menggunakan templat Obyek Transfer Data . Karenanya, DTO untuk menambahkan mobil akan terlihat seperti ini:
class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } }
* Menggunakan atribut validasi standar dari System.ComponentModel.DataAnnotations
.
Sudah waktunya untuk mencari tahu seperti apa Model Tampilan untuk halaman penambahan mobil. Beberapa pengembang dengan senang hati menyatakan bahwa TransportAddDTO itu sendiri akan demikian, namun, ini pada dasarnya salah, karena tidak ada yang dapat "dijejalkan" ke dalam kelas ini kecuali secara langsung untuk informasi backend yang diperlukan untuk menambahkan elemen baru (menurut definisi). Selain itu, data lain mungkin diperlukan pada halaman tambahkan: misalnya, direktori jenis kendaraan (berdasarkan mana TransportTypeId selanjutnya dinyatakan). Dalam hal ini, Model Tampilan berikut menunjukkan dirinya:
class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Di mana TransportTypeDTO dalam kasus ini akan menjadi pemetaan langsung TransportType (dan ini jauh dari selalu terjadi - baik dalam arah pemotongan maupun dalam arah ekspansi):
class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } }
Pada tahap ini, muncul pertanyaan yang masuk akal: di Razor dimungkinkan untuk mentransfer hanya satu model (dan terima kasih Tuhan), lalu bagaimana TransportAddDTO dapat digunakan untuk menghasilkan kode HTML di dalam halaman ini?
Sangat mudah! Cukup menambahkan, khususnya, DTO ini ke Model Tampilan , sesuatu seperti ini:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Sekarang masalah pertama dimulai. Mari kita coba menambahkan TextBox standar untuk "nomor kendaraan" ke halaman dalam file .cshtml kami (biarlah itu TransportAddView.cshtml):
@model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number)
Ini akan dirender menjadi kode HTML seperti ini:
<input id="AddDTO_Number" name="AddDTO.Number" />
Bayangkan bahwa bagian pengontrol dengan metode penambahan kendaraan terlihat seperti ini ( kode sesuai dengan MVC 5, untuk Core akan sedikit berbeda, tetapi intinya sama ):
[Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) {
Di sini kita melihat setidaknya dua masalah:
- Atribut Id dan Name memiliki awalan AddDTO , dan, kemudian, jika metode penambahan transport pada controller menggunakan prinsip binding model mencoba untuk mengikat data yang datang dari klien ke TransportAddDTO , maka objek di dalamnya akan seluruhnya terdiri dari nol (nilai default), yaitu itu akan menjadi contoh kosong baru. Itu logis - pengikat nama yang diharapkan dari formulir Nomor , bukan AddDTO_Number .
- Semua atribut meta hilang, mis. data-val-diperlukan dan yang lainnya yang kami uraikan dengan cermat di AddDTO sebagai atribut validasi. Bagi mereka yang menggunakan kekuatan penuh Razor, ini sangat penting, karena ini adalah kehilangan informasi yang signifikan untuk frontend.
Kami beruntung, dan mereka memiliki keputusan yang sesuai.
Hal-hal ini "berfungsi" saat menggunakan, misalnya, pembungkus untuk Kendo UI (mis. @Html.Kendo().TextBoxFor()
, dll.).
Mari kita mulai dengan masalah kedua: alasannya di sini adalah bahwa dalam Model Lihat, contoh TransportAddDTO yang ditransfer adalah nol . Dan implementasi mekanisme render sedemikian rupa sehingga atribut dalam hal ini dibaca setidaknya tidak sepenuhnya. Solusinya, masing-masing, jelas - pertama dalam Model Tampilan untuk menginisialisasi properti TransportAddDTO dengan turunan kelas menggunakan konstruktor default. Lebih baik melakukan ini di layanan yang mengembalikan Model Tampilan yang diinisialisasi, namun, sebagai bagian dari contoh, ia akan melakukan hal yang sama:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Setelah perubahan ini, hasilnya akan mirip dengan:
<input data-val="true" id="AddDTO_Number" name="AddDTO.Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Sudah lebih baik! Masih berurusan dengan masalah pertama - dengan itu, omong-omong, semuanya agak lebih rumit.
Untuk memahaminya, pertama-tama Anda perlu mencari tahu apa Razor (menyiratkan WebViewPage, sebuah contoh yang di dalamnya .cshtml tersedia sebagai ini ) adalah properti Html yang kita rujuk untuk memanggil TextBoxFor
.
Melihat itu, Anda dapat langsung memahami bahwa itu adalah tipe HtmlHelper<T>
, dalam kasus kami, HtmlHelper<TransportAddViewModel>
. Solusi yang mungkin untuk masalah muncul - untuk membuat HtmlHelper Anda sendiri di dalamnya, dan kirimkan TransportAddDTO kami sebagai masukan. Kami menemukan konstruktor sekecil mungkin untuk turunan dari kelas ini:
HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);
Kami dapat mengirimkan ViewContext langsung dari instance WebViewPage kami melalui this.ViewContext
. Sekarang mari kita mencari tahu di mana untuk mendapatkan instance kelas yang mengimplementasikan antarmuka IViewDataContainer. Misalnya, buat implementasi Anda:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } }
Seperti yang Anda lihat, sekarang kita mengalami ketergantungan pada beberapa objek yang diteruskan ke konstruktor untuk tujuan menginisialisasi ViewDataDictionary , karena semuanya sederhana di sini - ini adalah contoh TransportAddDTO kami dari Model Tampilan. Artinya, Anda bisa mendapatkan contoh yang dihargai seperti ini:
var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);
Karenanya, tidak ada masalah dalam membuat HtmlHelper baru:
var Helper = new HtmlHelper<T>(this.ViewContext, vdc);
Sekarang Anda dapat menggunakan yang berikut ini:
@model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number)
Ini akan dirender menjadi kode HTML seperti ini:
<input data-val="true" id="Number" name="Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Seperti yang Anda lihat, sekarang tidak ada masalah dengan elemen yang diberikan, dan siap untuk digunakan sepenuhnya. Tetap hanya untuk "menyisir" kode sehingga terlihat kurang tebal. Misalnya, kami memperluas ViewDataContainer kami sebagai berikut:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } public HtmlHelper<T> GetHtmlHelper(ViewContext context) { return new HtmlHelper<T>(context, this); } }
Kemudian dari Razor Anda dapat bekerja seperti ini:
@model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number)
Selain itu, tidak ada yang mau memperpanjang implementasi standar WebViewPage sehingga berisi properti yang diinginkan (dengan setter untuk instance kelas DTO).
Kesimpulan
Ini menyelesaikan masalah, dan juga mendapatkan arsitektur Model Tampilan untuk bekerja dengan Razor, yang berpotensi mengandung semua elemen yang diperlukan.
Perlu dicatat bahwa ViewDataContainer yang dihasilkan ternyata bersifat universal, dan cocok untuk digunakan.
Masih menambahkan beberapa tombol ke file .cshtml kami, dan tugas akan selesai (tidak mempertimbangkan pemrosesan pada backend'e). Ini saya usulkan untuk dilakukan sendiri.
Jika pembaca yang dihormati memiliki ide tentang bagaimana menerapkan apa yang dibutuhkan dengan cara yang lebih optimal, saya akan dengan senang hati mendengarkan komentar.
Salam
Peter Osetrov