MVC Razor 뷰 중첩 foreach의 모델


94

일반적인 시나리오를 상상해보십시오. 이것은 제가 겪고있는 것의 더 간단한 버전입니다. 나는 실제로 더 많은 중첩 층이 있습니다 ....

하지만 이것은 시나리오입니다

테마 포함 목록 범주 포함 목록 제품 포함 목록

My Controller는 해당 테마의 모든 카테고리,이 카테고리 내의 제품 및 주문과 함께 완전히 채워진 테마를 제공합니다.

주문 컬렉션에는 편집 가능해야하는 수량이라는 속성이 있습니다.

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

대신 람다를 사용하면 foreach 루프 내의 항목이 아닌 "Theme"라는 최상위 Model 개체에 대한 참조 만 얻는 것 같습니다.

내가하려는 것이 가능할까요? 아니면 가능한 일을 과대 평가하거나 오해 했습니까?

위의 경우 TextboxFor, EditorFor 등에서 오류가 발생합니다.

CS0411 : 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)'메서드의 형식 인수는 사용법에서 유추 할 수 없습니다. 형식 인수를 명시 적으로 지정해보십시오.

감사.


1
당신은 @모든 전에 가져야하지 foreach않습니까? Html.EditorFor( Html.EditorFor(m => m.Note)예 :) 및 나머지 메서드 에도 람다가 있어야하지 않습니까? 착각 할 수 있지만 실제 코드를 붙여 넣어 주시겠습니까? 저는 MVC를 처음 접했지만 부분보기 나 편집자 (이름이 맞다면?)로 쉽게 해결할 수 있습니다.
Kobi

category.name나는 그것이 확신 string하고 ...For첫 번째 매개 변수로 문자열을 지원하지 않습니다
balexandre

예, 방금 @를 놓쳤습니다. 감사. 그러나 람다의 경우 @ Html.TextBoxFor (m => m을 입력하기 시작하면 foreach 루프 내의 항목이 아닌 최상위 Model 개체에 대한 참조 만 얻는 것 같습니다.
David C

@DavidC-아직 대답 할 MVC 3에 대해 충분히 알지 못하지만 그게 당신의 문제라고 생각합니다 :).
Kobi

2
나는 기차를 타고 있지만 출근 할 때까지 답변이 없으면 답변을 게시합니다. 빠른 대답은 정기적를 사용하는 것 for()보다는보다 foreach. 그 이유를 설명 할게요, 그게 오랫동안 저를 혼란스럽게했기 때문입니다.
J. Holmes

답변:


304

빠른 대답은 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에서 작동?
  • 식 트리 란 무엇입니까?
  • Model Binder는 어떻게 작동합니까?

이 세 가지 개념이 모두 연결되어 답을 얻습니다.

: 방법을 LabelFor다른 ...For헬퍼는 MVC에서 작동?

그래서, 당신은 사용했습니다 HtmlHelper<T>에 대한 확장 LabelForTextBoxFor다른 사람을, 그리고 당신은 아마 당신이 그들을 호출 당신이 그들에게 람다를 통과 때 것으로 나타났습니다 마술 일부 HTML을 생성합니다. 하지만 어떻게?

따라서 가장 먼저 주목해야 할 것은 이러한 도우미의 서명입니다. 가장 간단한 과부하를 살펴 보겠습니다. TextBoxFor

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

첫째, 이것은 강력한 형식의 HtmlHelper, 형식에 대한 확장 메서드입니다 <TModel>. 따라서 장면 뒤에서 일어나는 일을 간단히 설명하기 위해 razor가이 뷰를 렌더링 할 때 클래스를 생성합니다. 이 클래스 내부에는의 인스턴스가 있습니다 HtmlHelper<TModel>(property로 Html사용할 수있는 이유입니다 @Html...). 여기서는 문에 TModel정의 된 유형 @model입니다. 따라서 귀하의 경우에는이 뷰를 볼 때 TModel 항상 유형이 ViewModels.MyViewModels.Theme됩니다.

자, 다음 논쟁은 조금 까다 롭습니다. 그래서 호출을 살펴 보겠습니다.

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

약간의 람다가있는 것 같습니다. 그리고 서명을 추측한다면이 인수의 유형은 단순히라고 생각할 수 있습니다. Func<TModel, TProperty>여기서는 TModel뷰 모델 TProperty 의 유형이며 속성의 유형으로 추론됩니다.

그러나 인수 의 실제 유형을 살펴보면 Expression<Func<TModel, TProperty>>.

따라서 일반적으로 람다를 생성 할 때 컴파일러는 다른 함수와 마찬가지로 람다를 가져 와서 MSIL로 컴파일합니다 (대리자, 메서드 그룹 및 람다는 코드 참조 일 뿐이므로 대체적으로 사용할 수있는 이유입니다.) .)

그러나 컴파일러가 유형이임을 확인 Expression<>하면 람다를 MSIL로 즉시 컴파일하지 않고 대신 식 트리를 생성합니다!

식 트리 란 무엇입니까 ?

그래서 도대체 표현 트리입니다. 글쎄, 그것은 복잡하지는 않지만 공원에서 산책하는 것도 아닙니다. ms를 인용하려면 :

| 식 트리는 트리와 유사한 데이터 구조의 코드를 나타냅니다. 여기서 각 노드는 식 (예 : 메서드 호출 또는 x <y와 같은 이진 연산)입니다.

간단히 말해서 표현식 트리는 "액션"모음으로서 함수를 표현한 것입니다.

의 경우 model=>model.SomeProperty표현식 트리에는 " '모델'에서 '일부 속성'가져 오기"라는 노드가 포함됩니다.

이 표현식 트리는 호출 할 수있는 함수 로 컴파일 할 수 있지만 표현식 트리 인 한 노드 모음 일뿐입니다.

그래서 그게 뭔데?

그래서 Func<>또는 Action<>일단 당신이 그것들을 가지고 있으면, 그것들은 거의 원자 적입니다. 당신이 정말로 할 수있는 것은 Invoke()그들에게 일명 그들이해야 할 일을하도록 지시하는 것뿐입니다.

Expression<Func<>>반면에 추가, 조작, 방문 또는 컴파일 및 호출 할 수있는 작업 모음을 나타냅니다 .

그럼 왜 나에게이 모든 것을 말합니까?

그래서 의가 무엇인지 이해 Expression<>하면 Html.TextBoxFor. 텍스트 상자를 렌더링 할 때 제공하는 속성 에 대해 몇 가지 사항을 생성해야 합니다. attributes유효성 검사를위한 속성 과 같은 것, 특히이 경우 태그 이름<input>지정할 필요가 있습니다.

표현식 트리를 "걷고"이름을 만들어이를 수행합니다. 따라서와 같은 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 쿼리)을 작성하여 유사한 작업을 수행합니다.)

Model Binder는 어떻게 작동합니까?

그러니 일단 이해하면 모델 바인더에 대해 간략하게 이야기해야합니다. 양식이 게시되면 단순히 평면과 같으며 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" />

어느, 당신은 큰 깊이 중첩 된 뷰 모델을 기대하는 행동이 양식을 게시하는 경우, 다음이라는 속성 수분 위해 노력할 것 FooBar의 오프 TModel. 기껏해야 거기에 있지 않고 최악의 경우 완전히 다른 것입니다. Baz루트 모델이 아닌 을 수락하는 특정 작업에 게시하는 경우이 방법이 잘 작동합니다! 실제로 부분은보기 컨텍스트를 변경하는 좋은 방법입니다. 예를 들어, 모두 다른 작업에 게시되는 여러 양식이있는 페이지가있는 경우 각 작업에 대한 부분을 렌더링하는 것이 좋습니다.


이제이 모든 것을 얻으면 Expression<>프로그램 적으로 확장하고 다른 깔끔한 작업을 수행 하여 를 사용하여 정말 흥미로운 작업을 시작할 수 있습니다. 나는 그것에 대해 다루지 않을 것입니다. 그러나 바라건대, 이것은 당신이이면에서 일어나는 일과 사물이 왜 그렇게 행동하는지에 대한 더 나은 이해를 제공 할 것입니다.


4
멋진 답변입니다. 나는 현재 그것을 소화하려고 노력하고 있습니다. :) 또한 Cargo Culting의 유죄입니다! 그 설명처럼.
David C

4
이 자세한 답변에 감사드립니다!
Kobi

14
이를 위해 하나 이상의 찬성표가 필요합니다. +3 (각 설명에 하나씩),화물 이교도에게는 +1. 절대적으로 훌륭한 대답!
Kyeotic

3
이것이 내가 SO를 사랑하는 이유입니다 : 짧은 답변 + 심층 설명 + 멋진 링크 (화물 컬트). 나는 물건의 내부 작동에 대한 지식이 매우 중요하다고 생각하지 않는 사람에게화물 컬트에 대한 게시물을 보여주고 싶습니다!
user1068352

18

간단히 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 도우미는 정렬 된 방식으로 요소의 이름을 생성하므로 게시 된 테마 엔티티 전체를 검색하는 데 더 이상 문제가 없습니다.


1
받아 들여지는 답변은 매우 좋은 답변이지만 (저도 찬성했습니다)이 답변은 유지 관리가 더 쉬운 옵션입니다.
Aaron

4

카테고리 부분과 제품 부분을 추가 할 수 있습니다. 각각은 기본 모델의 더 작은 부분을 자체 모델로 취합니다. 즉, 카테고리의 모델 유형이 IEnumerable 일 수 있으며 Model.Theme를 전달합니다. 제품의 부분은 (카테고리 부분 내에서) Model.Products를 전달하는 IEnumerable 일 수 있습니다.

이것이 올바른 방법인지는 잘 모르겠지만 알고 싶습니다.

편집하다

이 답변을 게시 한 이후로 EditorTemplates를 사용했으며 반복되는 입력 그룹 또는 항목을 처리하는 가장 쉬운 방법을 찾았습니다. 모든 유효성 검사 메시지 문제와 양식 제출 / 모델 바인딩 문제를 자동으로 처리합니다.


그것은 나에게 일어났습니다. 업데이트하기 위해 다시 읽을 때 어떻게 처리할지 확신하지 못했습니다.
David C

1
비슷하지만 단위로 게시 할 양식이므로 제대로 작동하지 않습니다. 부분 내부에서 뷰 컨텍스트가 변경되고 더 이상 중첩 된 표현식이 없습니다. Theme모델에 다시 게시 하면 제대로 수분이 공급되지 않습니다.
J. Holmes

그것도 제 걱정거리입니다. 나는 일반적으로 제품을 표시하는 읽기 전용 접근 방식으로 위의 작업을 수행 한 다음 각 제품에 대한 링크를 / Product / Edit / 123 작업 메서드에 제공하여 자체 양식에서 각각을 편집합니다. MVC의 한 페이지에서 너무 많은 일을하려고하면 되돌릴 수 있다고 생각합니다.
Adrian Thompson Phillips

@AdrianThompsonPhillips 예, 제가 가지고있는 것이 매우 가능합니다. 저는 Forms 배경에서 왔기 때문에 편집을 위해 페이지를 떠나야한다는 생각에 항상 익숙해지지는 않습니다. :(
David C

2

바인딩 된 모델에 대한 뷰 내에서 foreach 루프를 사용하는 경우 ... 모델이 나열된 형식이어야합니다.

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

위의 코드가 컴파일된다는 사실에 놀랐습니다. @ Html.LabelFor가 매개 변수로 FUNC 작업을 필요로, 당신은하지 않습니다
제나 잎

위의 코드가 컴파일되는지 여부는 모르겠지만 중첩 된 @foreach가 저에게 효과적 입니다. MVC5.
antonio

0

오류에서 분명합니다.

"For"가 추가 된 HtmlHelpers는 람다 식을 매개 변수로 예상합니다.

값을 직접 전달하는 경우 일반 값을 사용하는 것이 좋습니다.

예 :

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

0

훨씬 더 간단한 또 ​​다른 가능성은 속성 이름 중 하나가 잘못되었다는 것입니다 (아마도 방금 클래스에서 변경 한 이름). 이것이 RazorPages .NET Core 3에서 저를위한 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.