MVC Razor iç içe geçmiş foreach modeli görünümü


94

Yaygın bir senaryo hayal edin, bu karşıma çıkan şeyin daha basit bir versiyonu. Aslında benim üzerimde birkaç katman daha var ...

Ama senaryo bu

Tema içerir Liste Kategori içerir Liste Ürün içerir Liste

Denetleyicim, bu tema için tüm Kategoriler, bu kategorilerdeki Ürünler ve bunların siparişleri ile tam dolu bir Tema sağlar.

Sipariş koleksiyonunun düzenlenebilir olması gereken Miktar adlı bir özelliği vardır (diğerleri arasında).

@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)
      }
   }
}

Bunun yerine lambda kullanırsam, foreach döngüsündekilere değil, yalnızca üst Model nesnesine, "Tema" ya bir referans alıyorum.

Orada yapmaya çalıştığım şey mümkün mü yoksa mümkün olanı abarttım ya da yanlış anladım mı?

Yukarıdakilerle TextboxFor, EditorFor, vb.'de bir hata alıyorum.

CS0411: 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' yönteminin tür bağımsız değişkenleri kullanımdan çıkarılamaz. Tür bağımsız değişkenlerini açıkça belirtmeyi deneyin.

Teşekkürler.


1
Her @şeyden önce sahip olmanız gerekmez foreachmi? Ayrıca Html.EditorFor( Html.EditorFor(m => m.Note)örneğin) ve diğer yöntemlerde de lambdas olması gerekmez mi ? Yanılıyor olabilirim, ancak lütfen gerçek kodunuzu yapıştırır mısınız? MVC'de oldukça yeniyim, ancak bunu kısmi görünümlerle veya editörlerle (eğer isim buysa?) Oldukça kolay bir şekilde çözebilirsiniz.
Kobi

category.nameBunun bir eminim stringve ...Forilk parametre olarak bir dize desteklemez
balexandre

evet, sadece @ 'leri kaçırdım, şimdi eklendi. Teşekkürler. Bununla birlikte, lambda'ya gelince, @ Html.TextBoxFor (m => m.) Yazmaya başlarsam, foreach döngüsünün içindekilere değil, yalnızca üst Model nesnesine bir referans alıyorum.
David C

@DavidC - Henüz cevap verecek kadar MVC 3 bilmiyorum - ama bunun senin sorunun olduğundan şüpheleniyorum :).
Kobi

2
Trendeyim, ama işe geldiğimde bu cevaplanmazsa, bir cevap vereceğim. Hızlı cevap, a for()yerine normal kullanmaktır foreach. Nedenini açıklayacağım, çünkü uzun süre kafamı karıştırdı.
J. Holmes

Yanıtlar:


304

Hızlı cevap, döngülerinizin for()yerine bir döngü kullanmaktır foreach(). Gibi bir şey:

@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)
      }
   }
}

Ancak bu, bunun neden sorunu çözdüğünü açıklıyor .

Bu sorunu çözmeden önce en azından üstünkörü bir anlayışa sahip olduğunuz üç şey var. Bunu çerçeve ile çalışmaya başladığımda uzun bir süre kargo kültivasyonu yaptığımı itiraf etmeliyim . Ve gerçekten neler olup bittiğini anlamam epey zaman aldı.

Bu üç şey:

  • MVC'de LabelForve diğer ...Foryardımcılar nasıl çalışır?
  • İfade Ağacı nedir?
  • Model Bağlayıcı nasıl çalışır?

Bu kavramların üçü de bir cevap almak için birbirine bağlanır.

MVC'de LabelForve diğer ...Foryardımcılar nasıl çalışır?

Yani, kullandığınız HtmlHelper<T>için uzantıları LabelForve TextBoxForve diğerleri, ve muhtemelen bunları çağırmak, onlara bir lambda geçmek ve ne zaman fark sihirli bazı html üretir. Ama nasıl?

Yani ilk fark edilecek şey bu yardımcıların imzasıdır. Şunun için en basit aşırı yüklemeye bakalım TextBoxFor

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

Birincisi, bu, HtmlHelpertürü güçlü bir şekilde belirlenmiş bir tür için bir genişletme yöntemidir <TModel>. Yani, perde arkasında ne olduğunu basitçe ifade etmek gerekirse, ustura bu görüşü ortaya koyduğunda bir sınıf oluşturur. Bu sınıfın İçinde bir örneğidir HtmlHelper<TModel>(özelliği olarak Htmlkullanabilirsiniz yüzden, @Html...,) TModelsizin tanımlanan türüdür @modelaçıklamada. Yani sizin durumunuzda, bu görüşe baktığınızda TModel her zaman tipte olacaktır ViewModels.MyViewModels.Theme.

Şimdi, bir sonraki argüman biraz aldatıcı. Öyleyse bir çağrıya bakalım

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

Görünüşe göre küçük bir lambda varmış gibi görünüyor ve eğer biri imzayı tahmin edecekse, bu argümanın türünün basitçe a olacağını düşünebilir Func<TModel, TProperty>, burada TModelgörünüm modelinin TProperty türü ve özelliğin türü olarak çıkarılır.

Ancak argümanın gerçek türüne bakarsanız, bu pek doğru değil Expression<Func<TModel, TProperty>>.

Bu nedenle, normalde bir lambda oluşturduğunuzda, derleyici lambda'yı alır ve diğer işlevler gibi MSIL'de derler (bu nedenle, temsilciler, yöntem grupları ve lambdaları birbirinin yerine daha çok veya daha az kullanabilirsiniz, çünkü bunlar yalnızca kod referanslarıdır. .)

Ancak, derleyici türün bir an olduğunu gördüğünde, Expression<>lambda'yı hemen MSIL'e derlemez, bunun yerine bir İfade Ağacı oluşturur!

Bir nedir İfade Ağacı ?

Öyleyse, lanet olası bir ifade ağacıdır. Pekala, karmaşık değil ama parkta bir yürüyüş de değil. Ms alıntı yapmak için:

| İfade ağaçları, ağaç benzeri bir veri yapısındaki kodu temsil eder; burada her düğüm bir ifade, örneğin bir yöntem çağrısı veya x <y gibi bir ikili işlem.

Basitçe ifade etmek gerekirse, bir ifade ağacı, bir işlevin "eylemler" koleksiyonu olarak temsilidir.

Bu durumda model=>model.SomeProperty, ifade ağacının içinde "Modelden 'Bazı Özellik Al" yazan bir düğüm bulunur.

Bu ifade ağacı, çağrılabilen bir işlev olarak derlenebilir , ancak bir ifade ağacı olduğu sürece, yalnızca bir düğümler koleksiyonudur.

Peki bu ne için iyi?

Yani Func<>ya Action<>da onlara sahip olduğunuzda, hemen hemen atomiktirler. Gerçekten yapabileceğiniz tek şey Invoke()onlar, yani yapmaları gereken işi yapmalarını söylemek.

Expression<Func<>>diğer yandan eklenebilen, manipüle edilebilen, ziyaret edilebilen veya derlenip çağrılabilen bir eylemler koleksiyonunu temsil eder .

Öyleyse neden tüm bunları bana anlatıyorsun?

Yani an'ın ne olduğu anlayışıyla Expression<>geri dönebiliriz Html.TextBoxFor. Bir metin kutusu oluşturduğunda, ona verdiğiniz özellik hakkında birkaç şey üretmesi gerekir. Şeyler gibi attributesdoğrulama için mülkiyet ve özellikle bu durumda ne anlamaya ihtiyacı isim<input> etiketi.

Bunu ifade ağacını "yürüyerek" ve bir isim oluşturarak yapar. Yani, gibi bir ifade için model=>model.SomeProperty, istediğiniz özellikleri toplayan ve inşa ettiğiniz ifadeyi yürütür <input name='SomeProperty'>.

Daha karmaşık Örneğin, gibi model=>model.Foo.Bar.Baz.FooBar, bu oluşturabilir<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Mantıklı olmak? O sadece iş değil Func<>, ama nasıl öyle çalışmalarını burada önemlidir.

(LINQ to SQL gibi diğer çerçevelerin bir ifade ağacında yürüyüp farklı bir dilbilgisi oluşturarak benzer şeyler yaptığını unutmayın; bu durumda bir SQL sorgusu)

Model Bağlayıcı nasıl çalışır?

Yani bunu anladıktan sonra, kısaca model bağlayıcı hakkında konuşmalıyız. Form gönderildiğinde, sadece bir daire gibidir Dictionary<string, string>, iç içe geçmiş görünüm modelimizin sahip olabileceği hiyerarşik yapıyı kaybetmiş oluruz. Bu anahtar-değer çifti kombinasyonunu alıp bazı özelliklerle bir nesneyi yeniden sulandırmaya çalışmak model bağlayıcının görevidir. Bunu nasıl yapıyor? Gönderilen girişin "anahtarını" veya adını kullanarak bunu tahmin ettiniz.

Yani form gönderisi şöyle görünüyorsa

Foo.Bar.Baz.FooBar = Hello

Ve denilen bir modele gönderi yayınlıyorsunuz SomeViewModel, sonra yardımcının ilk başta yaptığının tersini yapıyor. "Foo" adlı bir özelliği arar. Sonra "Foo" dan "Bar" adlı bir mülk arar, sonra "Baz" ı arar ... vb ...

Son olarak, değeri "FooBar" türüne ayrıştırmaya ve onu "FooBar" a atamaya çalışır.

PHEW !!!

Ve voila, senin modelin var. Model Binder'in henüz oluşturduğu örnek, istenen Eyleme teslim edilir.


Yani çözümünüz işe yaramıyor çünkü Html.[Type]For()yardımcıların bir ifadeye ihtiyacı var. Ve sen onlara bir değer veriyorsun. Bu değer için bağlamın ne olduğu hakkında hiçbir fikri yoktur ve onunla ne yapacağını bilemez.

Şimdi bazı insanlar oluşturmak için parçaların kullanılmasını önerdi. Şimdi bu teoride işe yarayacak, ancak muhtemelen beklediğiniz gibi değil. Bir parçayı işlediğinizde, türünü değiştirirsiniz TModelçünkü farklı bir görünüm bağlamındasınız. Bu, mülkünüzü daha kısa bir ifadeyle tanımlayabileceğiniz anlamına gelir. Ayrıca, yardımcı, ifadeniz için isim oluşturduğunda, sığ olacağı anlamına gelir. Yalnızca verildiği ifadeye göre oluşturulur (tüm bağlama göre değil).

Diyelim ki az önce "Baz" (önceki örneğimizden) oluşturulmuş bir kısmınız var. Bu parçanın içinde şöyle diyebilirsiniz:

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

Ziyade

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

Bu, bunun gibi bir giriş etiketi oluşturacağı anlamına gelir:

<input name="FooBar" />

Bu formu, derinlemesine iç içe geçmiş bir ViewModel bekleyen bir eyleme gönderiyorsanız, o zaman FooBariptal edilen bir özelliği hidratlamaya çalışacaktır TModel. Hangisi en iyi ihtimalle orada değildir ve en kötüsü tamamen başka bir şeydir. BazKök model yerine a'yı kabul eden belirli bir eyleme gönderi gönderiyorsanız , bu harika olur! Aslında, bölümler görünüm bağlamınızı değiştirmenin iyi bir yoludur, örneğin, tümü farklı eylemlere gönderen birden çok forma sahip bir sayfanız varsa, her biri için bir bölüm oluşturmak harika bir fikir olacaktır.


Şimdi tüm bunları aldığınızda, Expression<>programatik olarak genişleterek ve onlarla başka düzgün şeyler yaparak gerçekten ilginç şeyler yapmaya başlayabilirsiniz . Ben bunlara girmeyeceğim. Ancak umarım bu size perde arkasında neler olup bittiğini ve olayların neden olduğu gibi davrandığını daha iyi anlamanızı sağlar.


4
Harika cevap. Şu anda onu sindirmeye çalışıyorum. :) Ayrıca Cargo Culting'den de suçlu! Bu açıklama gibi.
David C

4
Bu detaylı cevap için teşekkürler!
Kobi

14
Bunun için birden fazla olumlu oy gerekiyor. +3 (her açıklama için bir tane) ve Kargo Tarikatçıları için +1. Kesinlikle mükemmel cevap!
Kyeotic

3
Bu yüzden SO: kısa cevap + derinlemesine açıklama + harika bağlantı (kargo kült). Eşyaların iç işleyişi hakkındaki bilginin son derece önemli olduğunu düşünmeyen herkese kargo kültü hakkındaki yazıyı göstermek isterim!
user1068352

18

Bunu yapmak için EditorTemplates'i kullanabilirsiniz, denetleyicinizin görünüm klasöründe "EditorTemplates" adlı bir dizin oluşturmanız ve iç içe geçmiş varlıklarınızın her biri için ayrı bir görünüm (varlık sınıfı adı olarak adlandırılır) yerleştirmeniz gerekir.

Ana görünüm :

@model ViewModels.MyViewModels.Theme

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

Kategori görünümü (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

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

Ürün görünümü (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

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

ve bunun gibi

bu şekilde Html.EditorFor helper, öğenin adlarını sıralı bir şekilde oluşturacaktır ve bu nedenle, yayınlanan Tema varlığını bir bütün olarak almak için başka bir sorun yaşamayacaksınız.


1
Kabul edilen yanıt çok iyi olsa da (ben de oy verdim), bu yanıt daha sürdürülebilir bir seçenektir.
Aaron

4

Bir Kategori kısmi ve bir Ürün parçası ekleyebilirsiniz, her biri kendi modeli olarak ana modelin daha küçük bir bölümünü alır, yani Kategori'nin model türü bir IEnumerable olabilir, Model.Theme'yi ona iletirsiniz. Ürünün kısmı, Model.Products'a geçirdiğiniz bir IEnumerable olabilir (Kısmi Kategori içinden).

Bunun ileriye doğru yol olup olmadığından emin değilim, ama bilmekle ilgilenirim.

DÜZENLE

Bu cevabı yayınladığımdan beri EditorTemplates'i kullandım ve bunu tekrar eden giriş gruplarını veya öğeleri işlemenin en kolay yolunu buldum. Tüm doğrulama mesajı sorunlarınızı ve form gönderme / model bağlama sorunlarını otomatik olarak ele alır.


Bu benim başıma gelmişti, sadece güncellemek için tekrar okuduğumda bununla nasıl başa çıkacağından emin değildim.
David C

1
Yakındır, ancak bu bir birim olarak gönderilecek bir form olduğu için tam olarak doğru çalışmayacaktır. Kısmi içine girdikten sonra görünüm bağlamı değişmiştir ve artık derinlemesine iç içe geçmiş ifadeye sahip değildir. ThemeModele geri gönderi yapmak düzgün bir şekilde hidratlanmaz.
J. Holmes

Benim de endişem bu. Genellikle yukarıdakileri ürünleri görüntülemek için salt okunur bir yaklaşım olarak yapardım ve ardından her üründe, her birini kendi formunda düzenlemek için bir / Ürün / Düzenle / 123 eylem yöntemine bir bağlantı sağlardım. MVC'de bir sayfada çok fazla şey yapmaya çalışırken geri dönebileceğinizi düşünüyorum.
Adrian Thompson Phillips

@AdrianThompsonPhillips evet, sahip olmam çok olası. Formlar geçmişinden geldim, bu nedenle bir düzenleme yapmak için sayfadan ayrılmak zorunda kalma fikrine her zaman alışamıyorum. :(
David C

2

Bağlı model için görünüm içinde foreach döngüsünü kullandığınızda ... Modelinizin listelenen biçimde olması gerekir.

yani

@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>
            }
        }

Yukarıdaki kodun derlenmesine bile şaşırdım. @ Html.LabelFor, parametre olarak bir FUNC işlemi gerektirir, sizinki değildir
Jenna Leaf 15

Yukarıdaki kodun derlenip derlenmediğini bilmiyorum ama yuvalanmış @foreach benim için çalışıyor . MVC5.
antonio

0

Hatadan anlaşılıyor.

"For" ile eklenen HtmlHelpers, parametre olarak lambda ifadesini bekler.

Değeri doğrudan aktarıyorsanız, Normal olanı kullansanız iyi olur.

Örneğin

TextboxFor (....) yerine Textbox () kullanın

TextboxFor sözdizimi Html.TextBoxFor (m => m.Property) gibi olacaktır

Senaryonuzda, kullanabileceğiniz indeks vereceği için temel for döngüsünü kullanabilirsiniz.

@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)
      }
   }
}

0

Çok daha basit bir olasılık da mülk isimlerinizden birinin yanlış olmasıdır (muhtemelen sınıfta değiştirdiğiniz biri). RazorPages .NET Core 3'te benim için buydu.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.