MVC Razor melihat model foreach bersarang

94

Bayangkan skenario umum, ini adalah versi yang lebih sederhana dari apa yang saya temukan. Saya sebenarnya memiliki beberapa lapisan bersarang lebih lanjut di saya ....

Tapi ini skenarionya

Tema berisi Kategori Daftar berisi Produk Daftar berisi Daftar

Kontroler Saya menyediakan Tema yang terisi penuh, dengan semua Kategori untuk tema itu, Produk dalam kategori ini dan pesanannya.

Koleksi pesanan memiliki properti yang disebut Kuantitas (di antara banyak lainnya) yang perlu diedit.

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@foreach (var category in Model.Theme)
{
   @Html.LabelFor(category.name)
   @foreach(var product in theme.Products)
   {
      @Html.LabelFor(product.name)
      @foreach(var order in product.Orders)
      {
          @Html.TextBoxFor(order.Quantity)
          @Html.TextAreaFor(order.Note)
          @Html.EditorFor(order.DateRequestedDeliveryFor)
      }
   }
}

Jika saya menggunakan lambda, maka saya hanya mendapatkan referensi ke objek Model teratas, "Tema" bukan yang berada dalam loop foreach.

Apakah yang saya coba lakukan di sana mungkin atau apakah saya melebih-lebihkan atau salah paham tentang apa yang mungkin?

Dengan di atas saya mendapatkan kesalahan pada TextboxFor, EditorFor, dll

CS0411: Jenis argumen untuk metode 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' tidak dapat disimpulkan dari penggunaan. Coba tentukan argumen tipe secara eksplisit.

Terima kasih.

David C
sumber
1
Bukankah seharusnya Anda memiliki @sebelumnya foreach? Bukankah Anda juga harus memiliki lambda di Html.EditorFor( Html.EditorFor(m => m.Note), misalnya) dan metode lainnya? Saya mungkin salah, tetapi bisakah Anda menempelkan kode Anda yang sebenarnya? Saya cukup baru mengenal MVC, tetapi Anda dapat menyelesaikannya dengan cukup mudah dengan tampilan parsial, atau editor (apakah itu namanya?).
Kobi
category.nameSaya yakin itu a stringdan ...Fortidak mendukung string sebagai parameter pertama
balexandre
ya, saya baru saja melewatkan @, sekarang tambah. Terima kasih. Namun, untuk lambda, jika saya mulai mengetik @ Html.TextBoxFor (m => m. Maka saya sepertinya hanya mendapatkan referensi ke objek Model teratas, bukan yang berada dalam loop foreach.
David C
@DavidC - Saya belum cukup tahu tentang MVC 3 untuk menjawab - tapi saya curiga itu masalah Anda :).
Kobi
2
Saya di dalam kereta, tetapi jika ini tidak dijawab pada saat saya mulai bekerja dengan sakit kirimkan jawaban. Jawaban cepatnya adalah dengan menggunakan regular for()daripada a foreach. Saya akan menjelaskan mengapa, karena itu membingungkan saya untuk waktu yang lama juga.
J.Holmes

Jawaban:

304

Jawaban cepatnya adalah dengan menggunakan for()loop sebagai pengganti foreach()loop Anda . Sesuatu seperti:

@for(var themeIndex = 0; themeIndex < Model.Theme.Count(); themeIndex++)
{
   @Html.LabelFor(model => model.Theme[themeIndex])

   @for(var productIndex=0; productIndex < Model.Theme[themeIndex].Products.Count(); productIndex++)
   {
      @Html.LabelFor(model=>model.Theme[themeIndex].Products[productIndex].name)
      @for(var orderIndex=0; orderIndex < Model.Theme[themeIndex].Products[productIndex].Orders; orderIndex++)
      {
          @Html.TextBoxFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Quantity)
          @Html.TextAreaFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Note)
          @Html.EditorFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].DateRequestedDeliveryFor)
      }
   }
}

Tapi ini menjelaskan mengapa ini memperbaiki masalah.

Ada tiga hal yang setidaknya sepintas Anda pahami sebelum Anda dapat menyelesaikan masalah ini. Saya harus mengakui bahwa saya membudidayakan kargo ini untuk waktu yang lama ketika saya mulai bekerja dengan kerangka kerja. Dan saya butuh waktu cukup lama untuk benar-benar memahami apa yang sedang terjadi.

Ketiga hal tersebut adalah:

  • Bagaimana cara kerja pembantu LabelFordan lainnya ...Fordi MVC?
  • Apa itu Pohon Ekspresi?
  • Bagaimana cara kerja Model Binder?

Ketiga konsep ini saling terkait untuk mendapatkan jawaban.

Bagaimana cara kerja pembantu LabelFordan lainnya ...Fordi MVC?

Jadi, Anda telah menggunakan HtmlHelper<T>ekstensi untuk LabelFordan TextBoxFordan lainnya, dan Anda mungkin memperhatikan bahwa ketika Anda memanggilnya, Anda mengirimkan lambda dan secara ajaib menghasilkan beberapa html. Tapi bagaimana caranya?

Jadi hal pertama yang harus diperhatikan adalah tanda tangan untuk para pembantu ini. Mari kita lihat kelebihan yang paling sederhana untuk TextBoxFor

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

Pertama, ini adalah metode ekstensi untuk sangat diketik HtmlHelper, jenis <TModel>. Jadi, untuk sekadar menyatakan apa yang terjadi di balik layar, saat pisau cukur membuat tampilan ini, itu akan menghasilkan kelas. Di dalam kelas ini adalah turunan dari HtmlHelper<TModel>(sebagai properti Html, itulah sebabnya Anda dapat menggunakan @Html...), di mana TModeltipe yang ditentukan dalam @modelpernyataan Anda . Jadi dalam kasus Anda, ketika Anda melihat tampilan ini TModel akan selalu menjadi tipe ViewModels.MyViewModels.Theme.

Sekarang, argumen selanjutnya agak rumit. Jadi mari kita lihat sebuah doa

@Html.TextBoxFor(model=>model.SomeProperty);

Sepertinya kita memiliki sedikit lambda, Dan jika seseorang menebak tanda tangannya, orang mungkin berpikir bahwa tipe untuk argumen ini hanya akan menjadi a Func<TModel, TProperty>, di mana TModeladalah tipe model tampilan dan TProperty disimpulkan sebagai tipe properti.

Tapi itu kurang tepat, jika Anda melihat tipe sebenarnya dari argumennya Expression<Func<TModel, TProperty>>.

Jadi, ketika Anda biasanya menghasilkan lambda, kompilator mengambil lambda dan mengkompilasinya menjadi MSIL, sama seperti fungsi lainnya (itulah mengapa Anda dapat menggunakan delegasi, grup metode, dan lambda secara bergantian, karena mereka hanya referensi kode .)

Namun, ketika kompilator melihat bahwa tipenya adalah Expression<>, ia tidak segera mengkompilasi lambda ke MSIL, melainkan menghasilkan Pohon Ekspresi!

Apa itu Pohon Ekspresi ?

Jadi, apa sih pohon ekspresi itu. Yah, itu tidak rumit tapi juga bukan jalan-jalan di taman. Mengutip ms:

| Pohon ekspresi mewakili kode dalam struktur data seperti pohon, di mana setiap node adalah ekspresi, misalnya, panggilan metode atau operasi biner seperti x <y.

Sederhananya, pohon ekspresi adalah representasi fungsi sebagai kumpulan "tindakan".

Dalam kasus model=>model.SomeProperty, pohon ekspresi akan memiliki simpul di dalamnya yang berbunyi: "Dapatkan 'Beberapa Properti' dari 'model'"

Pohon ekspresi ini dapat dikompilasi menjadi fungsi yang dapat dipanggil, tetapi selama pohon ekspresi ini, itu hanya kumpulan node.

Jadi apa gunanya itu?

Jadi Func<>atau Action<>, begitu Anda memilikinya, mereka cukup atom. Yang dapat Anda lakukan hanyalah Invoke()mereka, alias memberi tahu mereka untuk melakukan pekerjaan yang seharusnya mereka lakukan.

Expression<Func<>>di sisi lain, mewakili sekumpulan tindakan, yang dapat ditambahkan, dimanipulasi, dikunjungi , atau disusun dan dipanggil.

Jadi kenapa kamu memberitahuku semua ini?

Jadi dengan pemahaman tentang apa Expression<>itu, kita bisa kembali ke Html.TextBoxFor. Saat merender kotak teks, ia perlu menghasilkan beberapa hal tentang properti yang Anda berikan. Hal seperti attributesdi properti untuk validasi, dan secara khusus dalam hal ini perlu untuk mencari tahu apa yang harus nama yang <input>tag.

Ini dilakukan dengan "berjalan" pada pohon ekspresi dan membangun sebuah nama. Jadi untuk ekspresi seperti model=>model.SomeProperty, itu berjalan ekspresi mengumpulkan properti yang Anda minta dan bangun <input name='SomeProperty'>.

Untuk contoh yang lebih rumit, seperti model=>model.Foo.Bar.Baz.FooBar, itu mungkin menghasilkan<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Masuk akal? Bukan hanya pekerjaan yang Func<>dilakukan, tetapi cara kerjanya penting di sini.

(Perhatikan kerangka kerja lain seperti LINQ hingga SQL melakukan hal serupa dengan menjalankan pohon ekspresi dan membangun tata bahasa yang berbeda, dalam hal ini kueri SQL)

Bagaimana cara kerja Model Binder?

Jadi setelah Anda mendapatkannya, kita harus membahas secara singkat tentang model binder. Saat formulir diposting, itu hanya seperti sebuah flat Dictionary<string, string>, kami telah kehilangan struktur hierarki yang mungkin dimiliki oleh model tampilan bersarang kami. Tugas pengikat model adalah mengambil kombo pasangan nilai-kunci ini dan mencoba menghidrasi ulang objek dengan beberapa properti. Bagaimana cara melakukannya? Anda dapat menebaknya, dengan menggunakan "kunci" atau nama input yang diposting.

Jadi kalau bentuk postingannya seperti

Foo.Bar.Baz.FooBar = Hello

Dan Anda memposting ke model bernama SomeViewModel, lalu melakukan kebalikan dari apa yang dilakukan helper di tempat pertama. Ini mencari properti yang disebut "Foo". Kemudian mencari properti bernama "Bar" dari "Foo", lalu mencari "Baz" ... dan seterusnya ...

Terakhir, ia mencoba untuk mengurai nilai menjadi tipe "FooBar" dan menetapkannya ke "FooBar".

PHEW !!!

Dan voila, Anda memiliki model Anda. Instance yang baru saja dibuat oleh Model Binder akan diserahkan ke Action yang diminta.


Jadi solusi Anda tidak berhasil karena Html.[Type]For()penolong membutuhkan ekspresi. Dan Anda hanya memberi mereka nilai. Ia tidak tahu apa konteksnya untuk nilai itu, dan ia tidak tahu apa yang harus dilakukan dengannya.

Sekarang beberapa orang menyarankan menggunakan parsial untuk merender. Sekarang secara teori ini akan berhasil, tetapi mungkin tidak seperti yang Anda harapkan. Saat Anda merender parsial, Anda mengubah tipe TModel, karena Anda berada dalam konteks tampilan yang berbeda. Ini berarti Anda dapat mendeskripsikan properti Anda dengan ekspresi yang lebih pendek. Ini juga berarti ketika helper menghasilkan nama untuk ekspresi Anda, itu akan menjadi dangkal. Itu hanya akan dihasilkan berdasarkan ekspresi yang diberikan (bukan seluruh konteks).

Jadi katakanlah Anda memiliki sebagian yang baru saja dirender "Baz" (dari contoh kita sebelumnya). Di dalam bagian itu Anda bisa mengatakan:

@Html.TextBoxFor(model=>model.FooBar)

Daripada

@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)

Itu berarti itu akan menghasilkan tag input seperti ini:

<input name="FooBar" />

Yang mana, jika Anda memposting formulir ini ke tindakan yang mengharapkan ViewModel bertingkat dalam yang besar, maka itu akan mencoba untuk menghidrasi properti yang disebut FooBaroff TModel. Yang terbaik tidak ada, dan yang terburuk adalah sesuatu yang sama sekali berbeda. Jika Anda memposting ke tindakan tertentu yang menerima Baz, daripada model root, maka ini akan bekerja dengan baik! Faktanya, parsial adalah cara yang baik untuk mengubah konteks tampilan Anda, misalnya jika Anda memiliki laman dengan beberapa formulir yang semuanya memposting ke tindakan berbeda, maka merender parsial untuk masing-masing adalah ide yang bagus.


Sekarang setelah Anda mendapatkan semua ini, Anda dapat mulai melakukan hal-hal yang sangat menarik Expression<>, dengan mengembangkannya secara terprogram dan melakukan hal-hal rapi lainnya dengannya. Saya tidak akan membahas semua itu. Tapi, mudah-mudahan, ini akan memberi Anda pemahaman yang lebih baik tentang apa yang terjadi di balik layar dan mengapa segala sesuatunya berjalan seperti itu.

J. Holmes
sumber
4
Balasan yang mengagumkan. Saat ini saya mencoba untuk mencernanya. :) Juga bersalah atas Cargo Culting! Seperti deskripsi itu.
David C
4
Terima kasih atas jawaban mendetail ini!
Kobi
14
Perlu lebih dari satu suara positif untuk ini. +3 (satu untuk setiap penjelasan), dan +1 untuk Cargo-Cultist. Jawaban yang sangat brilian!
Kyeotic
3
Inilah mengapa saya suka SO: jawaban singkat + penjelasan mendalam + tautan luar biasa (kultus kargo). Saya ingin menunjukkan postingan tentang kultus kargo kepada siapa pun yang tidak berpikir bahwa pengetahuan tentang cara kerja dalam suatu barang sangatlah penting!
pengguna1068352
18

Anda cukup menggunakan EditorTemplates untuk melakukan itu, Anda perlu membuat direktori bernama "EditorTemplates" di folder tampilan pengontrol Anda dan menempatkan tampilan terpisah untuk setiap entitas bertingkat Anda (dinamai sebagai nama kelas entitas)

Tampilan utama:

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@Html.EditorFor(Model.Theme.Categories)

Tampilan kategori (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Products)

Tampilan produk (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Orders)

dan seterusnya

dengan cara ini Html.EditorFor helper akan menghasilkan nama elemen secara berurutan dan oleh karena itu Anda tidak akan memiliki masalah lebih lanjut untuk mengambil entitas Tema yang diposting secara keseluruhan

Alireza Sabouri
sumber
1
Meskipun jawaban yang diterima sangat bagus (saya juga memberikan suara positif), jawaban ini adalah opsi yang lebih mudah dipertahankan.
Aaron
4

Anda dapat menambahkan sebagian Kategori dan sebagian Produk, masing-masing akan mengambil sebagian kecil dari model utama sebagai modelnya sendiri, yaitu jenis model Kategori mungkin berupa IEnumerable, Anda akan meneruskan Model.Theme ke dalamnya. Parsial Produk mungkin berupa IEnumerable yang Anda berikan ke Model.Produk masuk (dari dalam Parsial Kategori).

Saya tidak yakin apakah itu cara yang tepat untuk maju, tetapi saya tertarik untuk mengetahuinya.

EDIT

Sejak memposting jawaban ini, saya telah menggunakan EditorTemplates dan menemukan ini cara termudah untuk menangani grup atau item input yang berulang. Ini menangani semua masalah pesan validasi Anda dan masalah pengikatan formulir / model secara otomatis.

Adrian Thompson Phillips
sumber
Itu terpikir oleh saya, hanya tidak yakin bagaimana itu akan menanganinya ketika saya membacanya kembali untuk memperbarui.
David C
1
Hampir tidak mungkin, tetapi karena ini adalah formulir yang akan diposting sebagai unit, ini tidak akan berfungsi dengan baik. Setelah berada di dalam parsial, konteks tampilan telah berubah dan tidak lagi memiliki ekspresi yang sangat bertingkat. Posting kembali ke Thememodel tidak akan terhidrasi dengan baik.
J.Holmes
Itu juga perhatian saya. Saya biasanya melakukan hal di atas sebagai pendekatan hanya baca untuk menampilkan produk dan kemudian memberikan tautan pada setiap produk ke mungkin metode tindakan / Produk / Edit / 123 untuk mengedit masing-masing pada formulirnya sendiri. Saya pikir Anda bisa gagal karena mencoba melakukan terlalu banyak pada satu halaman di MVC.
Adrian Thompson Phillips
@AdrianThompsonPhillips ya sangat mungkin yang saya miliki. Saya berasal dari latar belakang Formulir, jadi saya masih tidak selalu terbiasa dengan gagasan harus meninggalkan halaman untuk mengedit. :(
David C
2

Saat Anda menggunakan foreach loop dalam tampilan untuk model terikat ... Model Anda seharusnya dalam format yang terdaftar.

yaitu

@model IEnumerable<ViewModels.MyViewModels>


        @{
            if (Model.Count() > 0)
            {            

                @Html.DisplayFor(modelItem => Model.Theme.FirstOrDefault().name)
                @foreach (var theme in Model.Theme)
                {
                   @Html.DisplayFor(modelItem => theme.name)
                   @foreach(var product in theme.Products)
                   {
                      @Html.DisplayFor(modelItem => product.name)
                      @foreach(var order in product.Orders)
                      {
                          @Html.TextBoxFor(modelItem => order.Quantity)
                         @Html.TextAreaFor(modelItem => order.Note)
                          @Html.EditorFor(modelItem => order.DateRequestedDeliveryFor)
                      }
                  }
                }
            }else{
                   <span>No Theam avaiable</span>
            }
        }
Pranav Labhe
sumber
Saya terkejut bahwa kode di atas bahkan dapat dikompilasi. @ Html.LabelFor memerlukan operasi FUNC sebagai parameter, milik Anda bukan
Jenna Leaf
Saya tidak tahu apakah kode di atas dikompilasi atau tidak, tetapi @foreach bersarang berfungsi untuk saya. MVC5.
antonio
0

Jelas dari kesalahannya.

HtmlHelpers yang ditambahkan dengan "For" mengharapkan ekspresi lambda sebagai parameter.

Jika Anda meneruskan nilai secara langsung, lebih baik gunakan nilai Normal.

misalnya

Alih-alih TextboxFor (....) gunakan Textbox ()

sintaks untuk TextboxFor akan menjadi seperti Html.TextBoxFor (m => m.Property)

Dalam skenario Anda, Anda dapat menggunakan dasar for loop, karena ini akan memberi Anda indeks untuk digunakan.

@for(int i=0;i<Model.Theme.Count;i++)
 {
   @Html.LabelFor(m=>m.Theme[i].name)
   @for(int j=0;j<Model.Theme[i].Products.Count;j++) )
     {
      @Html.LabelFor(m=>m.Theme[i].Products[j].name)
      @for(int k=0;k<Model.Theme[i].Products[j].Orders.Count;k++)
          {
           @Html.TextBoxFor(m=>Model.Theme[i].Products[j].Orders[k].Quantity)
           @Html.TextAreaFor(m=>Model.Theme[i].Products[j].Orders[k].Note)
           @Html.EditorFor(m=>Model.Theme[i].Products[j].Orders[k].DateRequestedDeliveryFor)
      }
   }
}
Manas
sumber
0

Kemungkinan lain yang lebih sederhana adalah salah satu nama properti Anda salah (mungkin yang baru saja Anda ubah di kelas). Inilah yang saya alami di RazorPages .NET Core 3.

FirstDivision
sumber