簡體   English   中英

MVC Razor 視圖嵌套 foreach 的模型

[英]MVC Razor view nested foreach's model

想象一個常見的場景,這是我遇到的一個更簡單的版本。 我實際上有幾層進一步嵌套在我的......

但這是場景

主題包含列表 類別包含列表 產品包含列表

我的控制器提供了一個完全填充的主題,包含該主題的所有類別、該類別中的產品及其訂單。

訂單集合有一個名為 Quantity 的屬性(以及許多其他屬性),它需要是可編輯的。

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

如果我改用 lambda,那么我似乎只能獲得對頂級模型對象的引用,“主題”而不是 foreach 循環中的那些。

我在那里嘗試做的事情是可能的,還是我高估或誤解了可能的事情?

有了上面的內容,我在 TextboxFor、EditorFor 等上遇到了錯誤

CS0411:無法從用法推斷方法“System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)”的類型參數。 嘗試明確指定類型參數。

謝謝。

快速回答是使用for()循環代替foreach()循環。 就像是:

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

但這掩蓋了為什么這可以解決問題。

在解決此問題之前,您至少需要粗略了解三件事。 我不得不承認,當我開始使用該框架時,我對這個很感興趣 我花了很長時間才真正了解正在發生的事情。

這三件事是:

  • LabelFor和其他...For助手如何在 MVC 中工作?
  • 什么是表達式樹?
  • 模型綁定器如何工作?

所有這三個概念都聯系在一起以獲得答案。

LabelFor和其他...For助手如何在 MVC 中工作?

因此,您已經為LabelForTextBoxFor等使用了HtmlHelper<T>擴展,並且您可能注意到,當您調用它們時,您向它們傳遞了一個 lambda,它神奇地生成了一些 html。 但是如何?

所以首先要注意的是這些助手的簽名。 讓我們看看最簡單的TextBoxFor重載

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

首先,這是<TModel>類型的強類型HtmlHelper的擴展方法。 因此,簡單地說明幕后發生的事情,當 razor 渲染此視圖時,它會生成一個類。 此類內部是HtmlHelper<TModel>的實例(作為屬性Html ,這就是您可以使用@Html... ),其中TModel是您的@model語句中定義的類型。 因此,在您的情況下,當您查看此視圖​​時, TModel將始終屬於ViewModels.MyViewModels.Theme類型。

現在,下一個論點有點棘手。 所以讓我們看一個調用

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

看起來我們有一個小 lambda,如果要猜測簽名,人們可能會認為這個參數的類型只是一個Func<TModel, TProperty> ,其中TModel是視圖模型的類型, TProperty是推斷為屬性的類型。

但這並不完全正確,如果您查看參數的實際類型,它的Expression<Func<TModel, TProperty>>

因此,當您通常生成 lambda 時,編譯器會使用 lambda 並將其編譯為 MSIL,就像任何其他函數一樣(這就是為什么您可以或多或少交替使用委托、方法組和 lambda,因為它們只是代碼引用.)

但是,當編譯器發現類型是Expression<> ,它不會立即將 lambda 編譯為 MSIL,而是生成一個表達式樹!

什么是表達式樹

那么,到底什么是表達式樹。 嗯,這並不復雜,但也不是在公園里散步。 引用ms:

| 表達式樹表示樹狀數據結構中的代碼,其中每個節點都是一個表達式,例如,一個方法調用或一個二元運算(如 x < y)。

簡單地說,表達式樹是將函數表示為“動作”的集合。

model=>model.SomeProperty的情況下,表達式樹中會有一個節點,上面寫着:“從 'model' 中獲取 'Some Property'”

這個表達式樹可以編譯成一個可以調用的函數,但是只要是一個表達式樹,它就只是節點的集合。

那有什么用呢?

所以Func<>Action<> ,一旦你有了它們,它們就幾乎是原子的。 你真正能做的就是Invoke()他們,也就是告訴他們做他們應該做的工作。

另一方面, Expression<Func<>>表示一組動作,可以附加、操作、 訪問編譯和調用這些動作。

那你為什么要告訴我這一切?

因此,了解了Expression<>是什么之后,我們可以回到Html.TextBoxFor 當它呈現一個文本框時,它需要生成一些關於你給它的屬性的東西。 諸如用於驗證的attributes上的屬性之類的事情,特別是在這種情況下,它需要弄清楚<input>標簽的名稱

它通過“遍歷”表達式樹並構建名稱來實現這一點。 因此,對於像model=>model.SomeProperty這樣的表達式,它會model=>model.SomeProperty表達式,收集您要求的屬性並構建<input name='SomeProperty'>

對於更復雜的示例,例如model=>model.Foo.Bar.Baz.FooBar ,它可能會生成<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

合理? 這不僅是工作的Func<>做,但如何做的工作是很重要的位置。

(注意 LINQ to SQL 等其他框架通過遍歷表達式樹並構建不同的語法來做類似的事情,在這種情況下是 SQL 查詢)

模型綁定器如何工作?

所以一旦你明白了,我們必須簡要地談談模型綁定器。 當表單被發布時,它就像一個平面Dictionary<string, string> ,我們失去了嵌套視圖模型可能具有的層次結構。 模型綁定器的工作是采用此鍵值對組合並嘗試重新水化具有某些屬性的對象。 它是如何做到這一點的? 您猜對了,通過使用發布的輸入的“鍵”或名稱。

所以如果表單帖子看起來像

Foo.Bar.Baz.FooBar = Hello

並且您要發布到一個名為SomeViewModel的模型,然后它會與助手最初所做的相反。 它尋找一個名為“Foo”的屬性。 然后它從“Foo”中尋找一個名為“Bar”的屬性,然后它尋找“Baz”……等等……

最后,它嘗試將值解析為“FooBar”的類型並將其分配給“FooBar”。

呼!!!

瞧,你有你的模型。 Model Binder 剛剛構建的實例被傳遞到請求的 Action 中。


所以你的解決方案不起作用,因為Html.[Type]For()助手需要一個表達式。 而你只是給了他們一個價值。 它不知道該值的上下文是什么,也不知道如何處理它。

現在有人建議使用局部渲染。 現在這在理論上可行,但可能不是您期望的方式。 當您渲染局部時,您正在更改TModel的類型,因為您處於不同的視圖上下文中。 這意味着您可以用更短的表達方式來描述您的財產。 這也意味着當助手為您的表達式生成名稱時,它會很淺。 它只會根據給定的表達式(而不是整個上下文)生成。

因此,假設您有一個部分渲染了“Baz”(來自我們之前的示例)。 在那個部分你可以說:

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

而不是

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

這意味着它將生成一個輸入標簽,如下所示:

<input name="FooBar" />

其中,如果您將此表單發布到一個需要大型深度嵌套 ViewModel 的操作,那么它將嘗試從TModel FooBar一個名為FooBar的屬性。 最好的情況是不存在,最壞的情況是完全不同的東西。 如果您要發布到接受Baz的特定操作,而不是根模型,那么這會很好用! 事實上,局部是改變你的視圖上下文的好方法,例如,如果你有一個頁面有多個表單,所有表單都發布到不同的動作,那么為每個表單渲染一個局部將是一個好主意。


現在一旦你掌握了所有這些,你就可以開始用Expression<>做真正有趣的事情,通過以編程方式擴展它們並用它們做其他巧妙的事情。 我不會涉足這些。 但是,希望這能讓您更好地了解幕后發生的事情以及事情為什么會這樣。

您可以簡單地使用 EditorTemplates 來做到這一點,您需要在控制器的視圖文件夾中創建一個名為“EditorTemplates”的目錄,並為每個嵌套實體放置一個單獨的視圖(命名為實體類名稱)

主要觀點:

@model ViewModels.MyViewModels.Theme

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

類別視圖(/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

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

產品視圖 (/MyController/EditorTemplates/Product.cshtml) :

@model ViewModels.MyViewModels.Product

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

等等

這樣 Html.EditorFor 助手將以有序的方式生成元素的名稱,因此您將不會有任何進一步的問題來檢索整個發布的 Theme 實體

您可以添加一個 Category 部分和一個 Product 部分,每個部分都會占用主模型的一小部分,因為它是自己的模型,即 Category 的模型類型可能是 IEnumerable,您可以將 Model.Theme 傳遞給它。 Product 的部分可能是一個 IEnumerable,您將 Model.Products 傳入(從 Category 部分中)。

我不確定這是否是正確的前進方向,但有興趣知道。

編輯

自從發布此答案后,我使用了 EditorTemplates 並發現這是處理重復輸入組或項目的最簡單方法。 它會自動處理您所有的驗證消息問題和表單提交/模型綁定問題。

當您在綁定模型的視圖中使用 foreach 循環時......您的模型應該是列出的格式。

IE

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

另一種更簡單的可能性是您的一個屬性名稱是錯誤的(可能是您剛剛在課堂上更改的)。 這就是 RazorPages .NET Core 3 中對我的意義。

從錯誤中可以看出。

附加有“For”的 HtmlHelpers 需要 lambda 表達式作為參數。

如果直接傳遞值,最好使用 Normal 。

例如

而不是 TextboxFor(....) 使用 Textbox()

TextboxFor 的語法類似於 Html.TextBoxFor(m=>m.Property)

在您的場景中,您可以使用基本的 for 循環,因為它會給您提供使用的索引。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM