Модель MVC, не привязанная к коллекции в POST

#asp.net-mvc #model-binding

#asp.net-mvc #привязка модели

Вопрос:

  • Visual Studio 2012
  • MVC 5.2.0
  • jQuery 2.1.1
  • Объединенный пользовательский интерфейс jQuery 1.10.4
  • Json.Net 6.0.3
  • Kendo UI MVC 2014.1.528

Я прочитал много похожих сообщений. Я пытался применить то, что я видел, но безрезультатно. Я бросаюсь к ногам ваших клавиатур для вашего милосердия и помощи.

У меня есть сайт, похожий на опрос, поэтому для простоты я составил пример проекта, чтобы дублировать проблему. Я тоже могу загрузить это.

У меня есть модель с дочерним объектом Address — там проблем нет — она привязывается.

В модели также есть коллекция вопросов. Она никогда не привязывается к post, и в этом проблема здесь.

Давайте начнем с моделей:

Сам опрос:

 public class Survey
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }
    public string Name { get; set; }
    [DisplayName("Phone #")]
    [StringLength(15)]
    [DataType(DataType.PhoneNumber)]
    public string ContactMethodPhone { get; set; }
    [DisplayName("Email")]
    [StringLength(120)]
    [EmailAddress]
    public string ContactMethodEmail { get; set; }
    public virtual Address Address { get; set; }

    public virtual List<Question> Questions { get; set; }
}
  

и адрес:

 public class Address
{
    [ScaffoldColumn(false)]
    public int AddressId { get; set; }

    [DisplayName("Address line 1")]
    [Required]
    [StringLength(200)]
    public string Street { get; set; }
    [DisplayName("Address line 2")]
    [StringLength(200)]
    public string Street2 { get; set; }
    [DisplayName(" ")]
    [StringLength(50)]
    public string ApartmentNum { get; set; }
    [DisplayName("Type")]
    [StringLength(50)]
    public string Tenement { get; set; }
    [DisplayName("City")]
    [Required]
    [StringLength(200)]
    public string City { get; set; }
    [DisplayName("Province/State")]
    [StringLength(20)]
    public string State { get; set; }
    [Required]
    [DisplayName("Postal/Zip Code")]
    [StringLength(10)]
    public string MailCode { get; set; }
    [DisplayName("Country")]
    [StringLength(30)]
    public string Country { get; set; }


    [NotMapped]
    public List<SelectListItem> Cities { get; set; }
    [NotMapped]
    public List<SelectListItem> Provinces { get; set; }
    [NotMapped]
    public List<SelectListItem> Countries { get; set; }
    [NotMapped]
    public List<SelectListItem> MailCodes { get; set; }
    [NotMapped]
    public List<SelectListItem> Tenements { get; set; }
}
  

и вопросы:

  public class Question
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }
    [ScaffoldColumn(false)]
    public int ImageID { get; set; }                        // Question Image
    [DisplayName("Question: ")]
    public string InformationIntakeGroupValue { get; set; } // Question: ie Did you graguate high school 
    public int ImageID_Topic { get; set; } // Topic Image
    [DisplayName("Topic: ")]
    public string InformationIntakeTopicValue { get; set; } // Topic: ie Education
    [ScaffoldColumn(false)]
    public string InformationIntakeTypeCode { get; set; }   // Type of question (date, bool, text)
    // below not filled by select;
    // the key from the EntityAffilliateIntake record insert
    public int PersonId { get; set; }                       // Person anwering question
    // this is the user response area
    [DisplayName("Answer: ")]
    public string InformationIntakeValue { get; set; }      
    [DisplayName("Choice: ")]
    public string InformationIntakeValueBool { get; set; }
    [DisplayName("Date: ")]
    public DateTime InformationIntakeValueDate { get; set; }

    [ForeignKey("Survey")]
    public int SurveyId { get; set; }
    public virtual Survey Survey { get; set; }

}
  

(Note: fyi, I’ve tried the models without foreign keys as well — but perhaps it’s not defined correctly)

The controller :

    // GET: /Inquiry/Create
    public ActionResult Create()
    {
        var geoIpData = Strings._download_serialized_json_data<GeoData>(StringConstants.UrlForGeoService);

        SurveyModel = new Survey
        {
            Id=1,
            Address = new AddressController().GetAddressModel(geoIpData.country, geoIpData.regionName, geoIpData.city, ""),
            Questions = new QuestionController().GetQuestions(1).ToList()
        };

        return View(SurveyModel);
    }

    // POST: /Inquiry/Create
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    public ActionResult Create([Bind(Include = "Id, Name, ContactMethodPhone, ContactMethodEmail, Address, Questions")] Survey survey)
    {
        if (ModelState.IsValid)
        {
            int i = 0;
        }
        if (survey.Address.Cities == null)
        {
            survey.Address.Cities = SurveyModel.Address.Cities;
            survey.Address.Countries = SurveyModel.Address.Countries;
            survey.Address.MailCodes = SurveyModel.Address.MailCodes;
            survey.Address.Provinces = SurveyModel.Address.Provinces;
            survey.Address.Tenements = SurveyModel.Address.Tenements;
        }
        if (survey.Questions == null)
        {
            survey.Questions = SurveyModel.Questions;
        }
        return View(survey);
    }
  

The view:

 @model BindCollection.Models.Survey

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

// I use <div class="container">, <fieldset> and <div class="row">
// instead of <div class="form-horizontal"> and <div class="form-group">

<div class="container">
    <fieldset>
        <legend></legend>
        <h4>Survey</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.Id)

        <div class="row">
            @Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2 col-md-2 col-lg-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name)
                @Html.ValidationMessageFor(model => model.Name)
            </div>
        </div>

        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12">
                Your address information:<br />
                <br />
            </div>
        </div>
        @* no problem here with address *@
        @{ var vddAddress = new ViewDataDictionary(ViewData) { TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = "Address" } };}
        @Html.Partial("_AddressPartial", Model.Address, @vddAddress)

        <hr />
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12">
                How we can contact you? :<br />
                <br />
            </div>
        </div>
        <div class="row">
            @Html.LabelFor(model => model.ContactMethodPhone, new { @class = "control-label col-sm-2 col-md-2 col-lg-2" })
            <div class="col-sm-10 col-md-10 col-lg-10">
                @Html.EditorFor(model => model.ContactMethodPhone)
                @Html.ValidationMessageFor(model => model.ContactMethodPhone)
            </div>
        </div>

        <div class="row">
            @Html.LabelFor(model => model.ContactMethodEmail, new { @class = "control-label col-sm-2 col-md-2 col-lg-2" })
            <div class="col-sm-10 col-md-10 col-lg-10">
                @Html.EditorFor(model => model.ContactMethodEmail)
                @Html.ValidationMessageFor(model => model.ContactMethodEmail)
            </div>
        </div>
        <hr />
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12">
                Some Questions<br />
                <br />
            </div>
        </div>
        @*Here is the evil one! Beware!*@
        @{ var vddQuestions = new ViewDataDictionary(ViewData) { TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = "Questions" } };}
        @Html.Partial("_QuestionsPartial", Model.Questions, @vddQuestions)

        <hr />
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </fieldset>
</div>
  

}

The address partial view is insignificant as there’s no problem there.

Here’s the partial view for the questions:

 @model IEnumerable<BindCollection.Models.Question>
@using BindCollection
@using Kendo.Mvc.UI

@{
    string CurrentTopic = string.Empty;
    bool FirstTime = true;
}

@foreach (var item in Model)
{
    if (CurrentTopic != item.InformationIntakeTopicValue)
    {
        CurrentTopic = item.InformationIntakeTopicValue;
        if (!FirstTime)
        {
            FirstTime = false;
            item.InformationIntakeTopicValue = string.Empty;
        }
    }
    else
    {
        item.InformationIntakeTopicValue = string.Empty;
    }
    @Html.EditorFor(m=>item, "Question")
 <br />
 <br />

}
  

and, of course, I made an EditorTemplate for a question, as you can see a few lines above…

 @model BindCollection.Models.Question

@Html.HiddenFor(m=>m.Id)       

   @{if (!string.IsNullOrWhiteSpace(Model.InformationIntakeTopicValue))
    {
        <hr />
        <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12">
                <br />
                <br />
                <h4>
                    @Html.DisplayFor(m => m.InformationIntakeTopicValue, new { @class = "control-label col-sm-10 col-md-10 col-lg-10" })
                </h4>
                <br />
            </div>
        </div>
    }}

    @*type of value to ask for is denoted by item.InformationIntakeTypeCode*@

    <div class="row">
        <div class="control-label col-sm-9 col-md-9 col-lg-9">
            @Html.DisplayFor(m => m.InformationIntakeGroupValue, null)
        </div>

    </div>
    <div class="row">
            <div class="col-sm-12 col-md-12 col-lg-12">
                @{

                    var outputBool = false;
                    var outputDate = false;
                    var outputText = false;

                   if(Model.InformationIntakeTypeCode.ToLower().Contains("date") )
                   {
                       outputDate = true;
                   }
                   else if (Model.InformationIntakeTypeCode.ToLower().Contains("bool"))
                   {
                       outputBool = true;
                   }
                   else if (Model.InformationIntakeTypeCode.ToLower().Contains("string"))
                   {
                       outputText = true;
                   }

                }
                @if(outputBool)
                {
                    @(Html.Kendo().DropDownListFor(m => m.InformationIntakeValueBool)
                    .HtmlAttributes(new { style ="width: 100px;", id="InformationIntakeValueBool" Model.Id.ToString() })
                    .DataTextField("Text")
                    .DataValueField("Value")
                    .BindTo(new List<SelectListItem>() {
                        new SelectListItem() {
                            Text = "Please Select...",
                            Value = "0"
                        },
                        new SelectListItem() {
                            Text = "Yes",
                            Value = "true"
                        },
                        new SelectListItem() {
                            Text = "No",
                            Value = "false"
                        }
                    }).Value("0")
                    )
                }
                @if(outputDate)
                {
                    @(Html.Kendo().DatePickerFor(m => m.InformationIntakeValueDate)
                            .HtmlAttributes(new { style = "width: 100px;", id="InformationIntakeValueDate" Model.Id.ToString() })
                    )
                }
                @if (outputText)
                {
                    @Html.Kendo().AutoCompleteFor(m => m.InformationIntakeValue).HtmlAttributes(new { style = "width: 80%;", id="InformationIntakeValue" Model.Id.ToString()})
                }
            </div>
    </div>
  

So… When I POST, an odd thing occurs. All form values are passed, but the ones for Questions look strange:

 Id  1
Name    sally
Address.Street  123 Avenue Steet
Address.Street2 Building C
Address.Tenement    Suite
Address.ApartmentNum    111
Address.City_input  Sarnia
Address.City    Sarnia
Address.State_input Ontario
Address.State   Ontario
Address.Country_input   Canada
Address.Country Canada
Address.MailCode_input  N6B 2K0
Address.MailCode    N6B 2K0
ContactMethodPhone  555-555-5555
ContactMethodEmail  r@r.com
Questions.item.Id   1
Questions.item.InformationIntakeValueBool   true
Questions.item.Id   2
Questions.item.InformationIntakeValueDate   2/4/2014
Questions.item.Id   3
Questions.item.InformationIntakeValue   Speckled
Questions.item.Id   4
Questions.item.InformationIntakeValueBool   true
Questions.item.Id   5
Questions.item.InformationIntakeValue   Lightly Toasted
Questions.item.Id   7
Questions.item.InformationIntakeValueBool   true
Questions.item.Id   8
Questions.item.InformationIntakeValue   Nothing!
Questions.item.Id   6
Questions.item.InformationIntakeValueBool   true
Questions.item.Id   9
Questions.item.InformationIntakeValueDate   6/29/2014
  

Я подумал, как я видел в других сообщениях, что элементы вопроса должны выглядеть следующим образом:

 Questions[0].Id 1
Questions[0].InformationIntakeValueBool true
Questions[1].Id 2
Questions[1].InformationIntakeValueDate 2/4/2014
Questions[2].Id 3
Questions[2].InformationIntakeValue Speckled
  

Итак, я не уверен, почему моя выглядит так.

На стороне сервера запрос показывает только одну переменную для каждого:

 Request.Form.Results View   Expanding the Results View will enumerate the IEnumerable   
    [0] "__RequestVerificationToken"    object {string}
    [1] "Id"    object {string}
    [2] "Name"  object {string}
    [3] "Address.Street"    object {string}
    [4] "Address.Street2"   object {string}
    [5] "Address.Tenement"  object {string}
    [6] "Address.ApartmentNum"  object {string}
    [7] "Address.City_input"    object {string}
    [8] "Address.City"  object {string}
    [9] "Address.State_input"   object {string}
    [10]    "Address.State" object {string}
    [11]    "Address.Country_input" object {string}
    [12]    "Address.Country"   object {string}
    [13]    "Address.MailCode_input"    object {string}
    [14]    "Address.MailCode"  object {string}
    [15]    "ContactMethodPhone"    object {string}
    [16]    "ContactMethodEmail"    object {string}
    [17]    "Questions.item.Id" object {string}
    [18]    "Questions.item.InformationIntakeValueBool" object {string}
    [19]    "Questions.item.InformationIntakeValueDate" object {string}
    [20]    "Questions.item.InformationIntakeValue" object {string}
  

Смотрите последние 4 элемента? Как насчет других записей?

Я предполагаю, что есть что-то странное с ViewDataDictionary, который я отправляю на частичный просмотр вопроса.

Любая помощь была бы оценена. Это должно быть простым…

Комментарии:

1. Всякий раз, когда я вижу вопрос, в котором говорится «модель не привязана к post», я думаю: «Держу пари, он использует частичное представление или foreach. в 95% случаев я прав. Вы используете оба.

2. хорошо … и … никто не должен использовать частичные представления? Я неправильно ее использую? Это интересное замечание, но не очень полезное. Я использую частичные представления, чтобы я мог повторно использовать эту логику в другом месте. Частичное представление моего адреса используется во многих формах. Мне больше не нужно думать об этом. Все адреса собираются одинаковым образом. Это работает.

3. Да, и вам придется возиться с HtmlPrefix, чтобы заставить его работать. Проще просто использовать EditorTemplate, который делает это за вас.

4. Посмотрите, пожалуйста, выше — я использую шаблон редактора.

5. Ты очень расстраивающий человек. Вы не используете шаблон редактора в тех частях, с которыми у вас возникли проблемы. В этом суть. Вы используете циклы partials и foreach. Используйте шаблоны редактора вместо partial и используйте шаблоны редактора вместо использования foreach, потому что EditorFor автоматически выполнит цикл по коллекции и вызовет ваш шаблон редактора для каждого элемента.

Ответ №1:

Ни частичные представления, ни инструкции foreach не содержат необходимой информации для правильной привязки коллекций. Я всегда использую EditorTemplates для всего. Тем более, что EditorTemplates автоматически выполняет итерацию по коллекции.

Однако, если вы привязаны и полны решимости использовать цикл, тогда используйте цикл for, а затем проиндексируйте модель. В вашем случае:

 @for(int i = 0; i < Model.Count; i  )
{
    if (CurrentTopic != Model[i].InformationIntakeTopicValue)
    ...
    @Html.EditorFor(m => Model[i]) // don't have to specify the template name
                                   // since it's the same name as the type
}
  

Тем не менее, я бы просто сделал это:

На ваш взгляд, сделайте это:

 @*Here is the evil one! Beware!*@

@Html.EditorFor(m => m.Questions)
  

Тогда ваш вопрос.cshtml, как обычно, который будет автоматически повторен редактором для

Однако в начале вопроса.cshtml (после объявления модели) добавьте следующий код, это все, что необходимо для достижения того, что вы пытаетесь сделать. Вам вообще не нужен частичный просмотр.

 @{
  if (ViewBag.CurrentTopic != Model.InformationIntakeTopicValue)
  {
      ViewBag.CurrentTopic = Model.InformationIntakeTopicValue;
  }
  else
  {
      Model.InformationIntakeTopicValue = string.Empty;
  }
}
  

Комментарии:

1. Я использую EditorTemplate — я пытался просто использовать EditorFor без имени шаблона (того же имени, что и тип), и это не сработало. Он просто попытался создать свой собственный шаблон на основе моей модели. Мне пришлось указать имя шаблона.

2. Я постараюсь сделать это вообще без частичного просмотра, с вашим модом для моей пустой логики темы.

3. Если это не вызывает ваш шаблон редактора, то для этого есть причина. Либо вы не размещаете ее в нужном месте, либо происходит что-то еще.

4. Мой шаблон редактора выполняет итерацию, но мне пришлось принудительно использовать уникальные идентификаторы элементов управления для библиотеки пользовательского интерфейса Kendo, чтобы подключить ее скрипты. Мне интересно, не портит ли это, в свою очередь, привязку? ‘Name’ всегда будет одним и тем же, но мне нужно заставить идентификаторы быть уникальными: см. id=»InformationIntakeValueBool» Model.Id.toString() и т.д. Выше.

5. Приложение нашло все другие шаблоны редактора (дата / Электронная почта / и т.д.) В каталоге Views Shared EditorTemplates, но не соответствующий редактор для моего типа. Я посмотрю поближе на случай, если я опечатался — завтра я загружу свое тестовое решение.