Bagaimana memetakan View Model kembali ke Model Domain dalam aksi POST?

87

Setiap artikel yang ditemukan di Internet tentang penggunaan ViewModels dan penggunaan Automapper memberikan panduan tentang pemetaan arah "Controller -> View". Anda mengambil model domain bersama dengan semua Daftar Pilihan menjadi satu ViewModel khusus dan meneruskannya ke tampilan. Itu jelas dan bagus.
Tampilan memiliki bentuk, dan akhirnya kita berada dalam aksi POST. Di sini semua Pengikat Model datang ke tempat kejadian bersama dengan [jelas] Model Tampilan lain yang [jelas] terkait dengan ViewModel asli setidaknya dalam bagian konvensi penamaan demi pengikatan dan validasi.

Bagaimana Anda memetakannya ke Model Domain Anda?

Biarlah itu tindakan penyisipan, kita bisa menggunakan Automapper yang sama. Tetapi bagaimana jika itu adalah tindakan pembaruan? Kita harus mengambil Entitas Domain kita dari Repositori, memperbarui propertinya sesuai dengan nilai di ViewModel dan menyimpannya ke Repositori.

TAMBAHAN 1 (9 Februari 2010): Terkadang, menetapkan properti Model tidak cukup. Harus ada tindakan yang diambil terhadap Model Domain sesuai dengan nilai Model Tampilan. Yaitu, beberapa metode harus dipanggil pada Model Domain. Mungkin, harus ada semacam lapisan Layanan Aplikasi yang berdiri di antara Pengontrol dan Domain untuk memproses Model Tampilan ...


Bagaimana mengatur kode ini dan di mana menempatkannya untuk mencapai tujuan berikut?

  • jaga pengontrol tetap tipis
  • menghormati latihan SoC
  • ikuti prinsip Desain Berdasarkan Domain
  • kering
  • bersambung ...
Anthony Serdyukov
sumber

Jawaban:

37

Saya menggunakan antarmuka IBuilder dan mengimplementasikannya menggunakan ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (implementasi) RebuildViewModel hanya memanggilBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

btw Saya tidak menulis ViewModel Saya menulis Input karena itu jauh lebih pendek, tapi
harapan itu tidak terlalu penting itu membantu

Pembaruan: Saya menggunakan pendekatan ini sekarang di Aplikasi Demo ProDinner ASP.net MVC , ini disebut IMapper sekarang, ada juga pdf yang disediakan di mana pendekatan ini dijelaskan secara rinci

Omu
sumber
Saya suka pendekatan ini. Satu hal yang tidak saya jelaskan adalah implementasi IBuilder, terutama terkait dengan aplikasi berjenjang. Misalnya, ViewModel saya memiliki 3 SelectLists. Bagaimana implementasi builder mengambil nilai daftar pilihan dari repositori?
Matt Murrell
@Matt Murrell lihat prodinner.codeplex.com Saya melakukan ini di sana, dan saya menyebutnya IMapper di sana, bukan IBuilder
Omu
6
Saya suka pendekatan ini, saya menerapkan contohnya di sini: gist.github.com/2379583
Paul Stovell
Menurut saya, ini tidak sesuai dengan pendekatan Model Domain. Sepertinya pendekatan CRUD untuk persyaratan yang tidak jelas. Bukankah sebaiknya kita menggunakan Factories (DDD) dan metode terkait dalam Model Domain untuk menyampaikan beberapa tindakan yang masuk akal? Dengan cara ini sebaiknya kita memuat entitas dari DB dan memperbaruinya sesuai kebutuhan, bukan? Jadi sepertinya itu tidak sepenuhnya benar.
Artyom
7

Alat seperti AutoMapper dapat digunakan untuk memperbarui objek yang ada dengan data dari objek sumber. Tindakan pengontrol untuk memperbarui mungkin terlihat seperti:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Terlepas dari apa yang terlihat dalam cuplikan di atas:

  • Data POST untuk melihat model + validasi dilakukan di ModelBinder (dapat ditambahkan dengan binding kustom)
  • Penanganan error (mis. Menangkap pengecualian akses data yang dilontarkan oleh Repositori) dapat dilakukan dengan filter [HandleError]

Tindakan pengontrol cukup tipis dan kekhawatiran dipisahkan: masalah pemetaan ditangani dalam konfigurasi AutoMapper, validasi dilakukan oleh ModelBinder dan akses data oleh Repositori.

PanJanek
sumber
6
Saya tidak yakin Automapper berguna di sini karena tidak dapat membalikkan perataan. Bagaimanapun, Model Domain bukanlah DTO sederhana seperti Model Tampilan, oleh karena itu mungkin tidak cukup untuk menetapkan beberapa properti ke dalamnya. Mungkin, beberapa tindakan harus dilakukan terhadap Model Domain sesuai dengan konten Model Tampilan. Namun, +1 untuk sharing cukup baik pendekatannya.
Anthony Serdyukov
@ Anton ValueInjecter dapat membalikkan perataan;)
Omu
dengan pendekatan ini Anda tidak menjaga pengontrol tipis, Anda melanggar SoC dan KERING ... seperti yang disebutkan Omu Anda harus memiliki lapisan terpisah yang peduli untuk hal-hal pemetaan.
Rookian
5

Saya ingin mengatakan bahwa Anda menggunakan kembali istilah ViewModel untuk kedua arah interaksi klien. Jika Anda telah cukup membaca kode ASP.NET MVC di alam liar, Anda mungkin telah melihat perbedaan antara ViewModel dan EditModel. Saya pikir itu penting.

Sebuah ViewModel mewakili semua informasi yang diperlukan untuk merender tampilan. Ini bisa mencakup data yang dirender di tempat non-interaktif statis dan juga data murni untuk melakukan pemeriksaan guna memutuskan apa yang akan dirender. Tindakan GET Pengontrol umumnya bertanggung jawab untuk mengemas ViewModel untuk Tampilannya.

EditModel (atau mungkin ActionModel) mewakili data yang diperlukan untuk melakukan tindakan yang ingin dilakukan pengguna untuk POST tersebut. Jadi EditModel benar-benar mencoba menggambarkan suatu tindakan. Ini mungkin akan mengecualikan beberapa data dari ViewModel dan meskipun terkait, saya pikir penting untuk menyadari bahwa mereka memang berbeda.

Satu Ide

Yang mengatakan Anda dapat dengan mudah memiliki konfigurasi AutoMapper untuk pergi dari Model -> ViewModel dan yang lain untuk pergi dari EditModel -> Model. Kemudian tindakan Controller yang berbeda hanya perlu menggunakan AutoMapper. Sungguh, EditModel dapat memiliki fungsi di atasnya untuk memvalidasi propertinya terhadap model dan menerapkan nilai tersebut ke Model itu sendiri. Itu tidak melakukan hal lain dan Anda memiliki ModelBinders di MVC untuk memetakan Permintaan ke EditModel.

Ide Lain

Di luar itu, sesuatu yang baru-baru ini saya pikirkan seperti itu bekerja dari gagasan ActionModel adalah bahwa apa yang klien posting kembali kepada Anda sebenarnya adalah deskripsi beberapa tindakan yang dilakukan pengguna dan bukan hanya satu gumpalan besar data. Ini tentu akan membutuhkan beberapa Javascript di sisi klien untuk mengelola tetapi menurut saya idenya menarik.

Pada dasarnya saat pengguna melakukan tindakan pada layar yang Anda tunjukkan, Javascript akan mulai membuat daftar objek tindakan. Contohnya adalah mungkin pengguna berada di layar informasi karyawan. Mereka memperbarui nama belakang dan menambahkan alamat baru karena karyawan tersebut baru saja menikah. Di bawah sampul ini menghasilkan sebuah ChangeEmployeeNamedan AddEmployeeMailingAddressobjek ke daftar. Pengguna mengklik 'Simpan' untuk melakukan perubahan dan Anda mengirimkan daftar dua objek, masing-masing hanya berisi informasi yang diperlukan untuk melakukan setiap tindakan.

Anda akan membutuhkan ModelBinder yang lebih cerdas daripada yang default tetapi serializer JSON yang baik harus dapat menangani pemetaan objek aksi sisi klien ke sisi server. Yang di sisi server (jika Anda berada dalam lingkungan 2 tingkat) dapat dengan mudah memiliki metode yang menyelesaikan tindakan pada Model tempat mereka bekerja. Jadi tindakan Pengontrol akhirnya hanya mendapatkan Id untuk ditarik contoh Model dan daftar tindakan yang harus dilakukan di atasnya. Atau tindakan memiliki id di dalamnya untuk membuatnya sangat terpisah.

Jadi mungkin sesuatu seperti ini terwujud di sisi server:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

Itu benar-benar membuat tindakan posting kembali cukup umum karena Anda mengandalkan ModelBinder untuk memberi Anda instance IUserAction yang benar dan instance IUserAction Anda untuk melakukan logika yang benar itu sendiri atau (lebih mungkin) memanggil Model dengan info tersebut.

Jika Anda berada dalam lingkungan 3 tingkat, IUserAction dapat dibuat menjadi DTO sederhana untuk ditembakkan melintasi batas dan dilakukan dengan metode serupa pada lapisan aplikasi. Bergantung pada bagaimana Anda melakukan lapisan itu, lapisan itu dapat dipecah dengan sangat mudah dan masih tetap dalam transaksi (yang terlintas di benak adalah permintaan / tanggapan Agatha dan memanfaatkan peta identitas DI dan NHibernate).

Bagaimanapun saya yakin itu bukan ide yang sempurna, itu akan membutuhkan beberapa JS di sisi klien untuk mengelola, dan saya belum dapat melakukan proyek untuk melihat bagaimana itu terungkap, tetapi posting mencoba untuk memikirkan bagaimana caranya sampai di sana dan kembali lagi jadi saya pikir saya akan memberikan pikiran saya. Saya harap ini membantu dan saya akan senang mendengar cara lain untuk mengelola interaksi.

Sean Copenhaver
sumber
Menarik. Mengenai perbedaan antara ViewModel dan EditModel ... apakah Anda menyarankan bahwa untuk fungsi edit, Anda akan menggunakan ViewModel untuk membuat formulir, lalu mengikat ke EditModel saat pengguna mempostingnya? Jika demikian, bagaimana Anda menangani situasi di mana Anda perlu memposting ulang formulir karena kesalahan validasi (misalnya ketika ViewModel berisi elemen untuk mengisi drop-down) - apakah Anda hanya akan menyertakan elemen drop-down di EditModel juga? Dalam hal ini, apa perbedaan antara keduanya?
UpTheCreek
Saya menduga kekhawatiran Anda adalah jika saya menggunakan EditModel dan terjadi kesalahan, maka saya harus membangun kembali ViewModel saya yang bisa sangat mahal. Saya akan mengatakan hanya membangun kembali ViewModel dan memastikan itu memiliki tempat untuk meletakkan pesan pemberitahuan pengguna (mungkin yang positif dan negatif seperti kesalahan validasi). Jika ternyata menjadi masalah kinerja, Anda selalu dapat menyimpan ViewModel ke dalam cache hingga permintaan sesi berikutnya berakhir (mungkin adalah kiriman dari EditModel).
Sean Copenhaver