2016-01-22 7 views
6

In questo esempio ASP.Net MVC 4 programma Ho un utente che specifica i dettagli di una corsa di cavalli. La razza ha un nome e una lista di cavalli coinvolti. Ogni cavallo ha un nome e un'età.Come posso ottenere i messaggi di convalida per il rendering sulle proprietà della raccolta quando si utilizzano nuovi indici guid ogni volta?

Il modulo utilizza ajax e javascript per consentire alla persona di aggiungere e cancellare campi di input cavallo al volo, che viene quindi inviato tutto in una volta quando viene premuto il pulsante di invio.

Per semplificare questo processo per me, sto utilizzando un html helper effettuato da Matt Lunn.

public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string htmlFieldName = null) where TModel : class 
{ 
    var items = expression.Compile()(html.ViewData.Model); 
    var sb = new StringBuilder(); 

    if (String.IsNullOrEmpty(htmlFieldName)) 
    { 
     var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix; 

     htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression); 
    } 

    foreach (var item in items) 
    { 
     var dummy = new { Item = item }; 
     var guid = Guid.NewGuid().ToString(); 

     var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item")); 
     var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters); 

     sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid)); 
     sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid))); 
    } 

    return new MvcHtmlString(sb.ToString()); 
} 

Mentre io non capisco tutti i dettagli (si prega di leggere il post sul blog), so che cambia i valori di indice in GUID piuttosto che interi sequenziali. Ciò mi consente di eliminare gli elementi nel mezzo dell'elenco senza dover ricalcolare gli indici.

Ecco il resto del mio codice per la mia MCVE

HomeController.cs

public class HomeController : Controller 
{ 
    [HttpGet] 
    public ActionResult Index() 
    { 
     var model = new Race(); 

     //start with one already filled in 
     model.HorsesInRace.Add(new Horse() { Name = "Scooby", Age = 10 }); 

     return View(model); 
    } 

    [HttpPost] 
    public ActionResult Index(Race postedModel) 
    { 
     if (ModelState.IsValid) 
      //model is valid, redirect to another page 
      return RedirectToAction("ViewHorseListing"); 
     else 
      //model is not valid, show the page again with validation errors 
      return View(postedModel); 
    } 

    [HttpGet] 
    public ActionResult AjaxMakeHorseEntry() 
    { 
     //new blank horse for ajax call 
     var model = new List<Horse>() { new Horse() }; 
     return PartialView(model); 
    } 
} 

Models.cs

public class Race 
{ 
    public Race() { HorsesInRace = new List<Horse>(); } 

    [Display(Name = "Race Name"), Required] 
    public string RaceName { get; set; } 

    [Display(Name = "Horses In Race")] 
    public List<Horse> HorsesInRace { get; set; } 
} 

public class Horse 
{ 
    [Display(Name = "Horse's Name"), Required] 
    public string Name { get; set; } 

    [Display(Name = "Horse's Age"), Required] 
    public int Age { get; set; } 
} 

Index.cshtml

@model CollectionAjaxPosting.Models.Race 
<h1>Race Details</h1> 
@using (Html.BeginForm()) 
{ 
    @Html.ValidationSummary() 
    <hr /> 
    <div> 
     @Html.DisplayNameFor(x => x.RaceName) 
     @Html.EditorFor(x => x.RaceName) 
     @Html.ValidationMessageFor(x => x.RaceName) 
    </div> 
    <hr /> 
    <div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace)</div> 
    <button id="btn-add-horse" type="button">Add New Horse</button> 
    <input type="submit" value="Enter Horses" /> 
} 

<script type="text/javascript"> 
    $(document).ready(function() { 

     //add button logic 
     $('#btn-add-horse').click(function() { 
      $.ajax({ 
       url: '@Url.Action("AjaxMakeHorseEntry")', 
       cache: false, 
       method: 'GET', 
       success: function (html) { 
        $('#horse-listing').append(html); 
       } 
      }) 
     }); 

     //delete-horse buttons 
     $('#horse-listing').on('click', 'button.delete-horse', function() { 
      var horseEntryToRemove = $(this).closest('div.horse'); 
      horseEntryToRemove.prev('input[type=hidden]').remove(); 
      horseEntryToRemove.remove(); 
     }); 

    }); 
</script> 

Visualizzazioni/condiviso/EditorTemplates/Horse.c shtml

@model CollectionAjaxPosting.Models.Horse 

<div class="horse"> 
    <div> 
     @Html.DisplayNameFor(x => x.Name) 
     @Html.EditorFor(x => x.Name) 
     @Html.ValidationMessageFor(x => x.Name) 
    </div> 
    <div> 
     @Html.DisplayNameFor(x => x.Age) 
     @Html.EditorFor(x => x.Age) 
     @Html.ValidationMessageFor(x => x.Age) 
    </div> 
    <button type="button" class="delete-horse">Remove Horse</button> 
    <hr /> 
</div> 

Vista/Home/AjaxMakeHorseEntry.cshtml flusso

@model IEnumerable<CollectionAjaxPosting.Models.Horse> 

@Html.EditorForMany(x => x, "HorsesInRace") 

I dati funziona con questo codice. Una persona è in grado di creare ed eliminare voci di cavalli tanto quanto vogliono sulla pagina, e quando il modulo viene inviato, tutti i valori inseriti vengono assegnati al metodo di azione.

Tuttavia, se l'utente non entra nelle informazioni [Required] su una voce cavallo, ModelState.IsValid sarà falso che mostra ancora una volta la forma, ma non messaggi di convalida sarà mostrato per le proprietà del cavallo. L'errore di convalida viene visualizzato nell'elenco ValidationSummary.

Ad esempio, se viene lasciato vuoto, insieme a uno Horse's Name, verrà visualizzato un messaggio di convalida per il primo. Quest'ultimo avrà una validazione <span> con la classe "validazione campo valida".

enter image description here

Sono molto sicuro che questo è causato perché il metodo EditorForMany crea nuovi GUID per ogni proprietà ogni volta che viene creata la pagina, in modo messaggi di convalida non possono essere abbinate al campo corretto.

Cosa posso fare per risolvere questo problema? Devo abbandonare la creazione dell'indice guida o è possibile apportare una modifica al metodo EditorForMany per consentire il corretto inoltro dei messaggi di convalida?

risposta

6

Sono molto sicuro che questo è causato perché il metodo EditorForMany crea nuovi GUID per ogni proprietà ogni volta che viene creata la pagina, in modo messaggi di convalida non possono essere abbinate al campo corretto.

Sì; questo è esattamente ciò che sta accadendo qui.

Per risolvere questo problema, è necessario modificare EditorForMany() in modo che riutilizzi il GUID per un elemento, anziché generarne uno nuovo. A sua volta, questo significa che dobbiamo tenere traccia di quale GUID è stato assegnato a quale elemento, in modo che possa essere riutilizzato.

Il primo può essere eseguito mediante modifiche interne a EditorForMany(). Quest'ultimo ci impone di:

  1. aggiungere un alloggio ai nostri modelli in cui il GUID assegnato può essere memorizzato
  2. aggiungere un parametro al EditorForMany() per dire l'assistente, che proprietà contiene il GUID di ri-uso (se del caso).

Questo lascia l'helper EditorForMany simile a questo;

public static class HtmlHelperExtensions 
{ 
    public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, string htmlFieldName = null) where TModel : class 
    { 
     htmlFieldName = htmlFieldName ?? ExpressionHelper.GetExpressionText(propertyExpression); 

     var items = propertyExpression.Compile()(html.ViewData.Model); 
     var htmlBuilder = new StringBuilder(); 
     var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName); 
     Func<TValue, string> indexResolver = null; 

     if (indexResolverExpression == null) 
     { 
      indexResolver = x => null; 
     } 
     else 
     { 
      indexResolver = indexResolverExpression.Compile(); 
     } 

     foreach (var item in items) 
     { 
      var dummy = new { Item = item }; 
      var guid = indexResolver(item); 
      var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item")); 
      var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters); 

      if (String.IsNullOrEmpty(guid)) 
      { 
       guid = Guid.NewGuid().ToString(); 
      } 
      else 
      { 
       guid = html.AttributeEncode(guid); 
      } 

      htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid)); 

      if (indexResolverExpression != null) 
      { 
       htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression))); 
      } 

      htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid))); 
     } 

     return new MvcHtmlString(htmlBuilder.ToString()); 
    } 
} 

Abbiamo anche bisogno di cambiare i nostri modelli, per aggiungere una proprietà in cui è memorizzato il GUID;

public class Race 
{ 
    public Race() { HorsesInRace = new List<Horse>(); } 

    [Display(Name = "Race Name"), Required] 
    public string RaceName { get; set; } 

    [Display(Name = "Horses In Race")] 
    public List<Horse> HorsesInRace { get; set; } 
} 

public class Horse 
{ 
    [Display(Name = "Horse's Name"), Required] 
    public string Name { get; set; } 

    [Display(Name = "Horse's Age"), Required] 
    public int Age { get; set; } 

    // Note the addition of Index here. 
    public string Index { get; set; } 
}  

... e, infine, cambiare il nostro utilizzo di EditorForMany() di utilizzare la nuova firma;

Index.cshtml;

<div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace, x => x.Index)</div> 

AjaxMakeHorseEntry.cshtml;

@Html.EditorForMany(x => x, x => x.Index, "HorsesInRace") 

... che dovrebbe quindi far apparire i messaggi di convalida.


Per inciso, io non consiglia di utilizzare il parametro htmlFieldName per EditorForMany, e invece cambiare la vostra azione di controllo a;

[HttpGet] 
public ActionResult AjaxMakeHorseEntry() 
{ 
    var model = new Race(); 

    model.HorsesInRace.Add(new Horse()); 
    return PartialView(model); 
} 

... allora la vostra AjaxMakeHorseEntry.cshtml al fine di essere solo;

@model Models.Race 

@Html.EditorForMany(x => x.HorsesInRace, x => x.Index) 

Altrimenti, i generati name attributi rompono quando annidamento utilizzo di EditorForMany().

ho intenzione di aggiornare il post sul blog di utilizzare la versione sopra di EditorForMany(), meno l'accettazione del parametro htmlFieldName, per questo motivo.