2010-02-02 17 views
23

Ho la seguente azione di controllo:modello personalizzato legante per una proprietà

[HttpPost] 
public ViewResult DoSomething(MyModel model) 
{ 
    // do something 
    return View(); 
} 

Dove MyModel assomiglia a questo:

public class MyModel 
{ 
    public string PropertyA {get; set;} 
    public IList<int> PropertyB {get; set;} 
} 

Così DefaultModelBinder dovrebbe legare questo senza un problema. L'unica cosa è che voglio usare il raccoglitore speciale/personalizzato per il binding PropertyB e voglio anche riutilizzare questo raccoglitore. Quindi ho pensato che la soluzione sarebbe stata quella di mettere un attributo ModelBinder prima della proprietà B che ovviamente non funziona (l'attributo ModelBinder non è consentito su una proprietà). Vedo due soluzioni:

  1. Per utilizzare parametri di azione su ogni singola proprietà, invece di tutto il modello (che non preferirei come il modello ha un sacco di proprietà) come questo:

    public ViewResult DoSomething(string propertyA, [ModelBinder(typeof(MyModelBinder))] propertyB) 
    
  2. per creare un nuovo tipo permette di dire MyCustomType: List<int> e registrare il modello legante per questo tipo (questa è un'opzione)

  3. Forse per creare un legante per MyModel, override BindProperty e se la proprietà è "PropertyB" legare la proprietà con il mio raccoglitore personalizzato. È possibile?

C'è qualche altra soluzione?

risposta

14

esclusione BindProperty e se la proprietà è "PropertyB" associare la proprietà con il mio raccoglitore personalizzato

Questa è una buona soluzione, anche se invece di controllare "è PropertyB" è meglio controllare per la propria attributi personalizzati che definiscono leganti a livello di proprietà, come

[PropertyBinder(typeof(PropertyBBinder))] 
public IList<int> PropertyB {get; set;} 

È possibile vedere un esempio di BindProperty di override here.

13

In realtà mi piace solo la terza soluzione, la renderò una soluzione generica per tutti i ModelBinders, inserendola in un raccoglitore personalizzato che eredita da DefaultModelBinder ed è configurato per essere il raccoglitore modello predefinito per l'applicazione MVC.

Quindi si dovrebbe fare in modo che questo nuovo DefaultModelBinder associ automaticamente qualsiasi proprietà decorata con un attributo PropertyBinder, utilizzando il tipo fornito nel parametro.

Ho avuto l'idea da questo eccellente articolo: http://aboutcode.net/2011/03/12/mvc-property-binder.html.

Sarò anche voi mostrare il mio prendere sulla soluzione:

mio DefaultModelBinder:

namespace MyApp.Web.Mvc 
{ 
    public class DefaultModelBinder : System.Web.Mvc.DefaultModelBinder 
    { 
     protected override void BindProperty(
      ControllerContext controllerContext, 
      ModelBindingContext bindingContext, 
      PropertyDescriptor propertyDescriptor) 
     { 
      var propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor); 
      if (propertyBinderAttribute != null) 
      { 
       var binder = CreateBinder(propertyBinderAttribute); 
       var value = binder.BindModel(controllerContext, bindingContext, propertyDescriptor); 
       propertyDescriptor.SetValue(bindingContext.Model, value); 
      } 
      else // revert to the default behavior. 
      { 
       base.BindProperty(controllerContext, bindingContext, propertyDescriptor); 
      } 
     } 

     IPropertyBinder CreateBinder(PropertyBinderAttribute propertyBinderAttribute) 
     { 
      return (IPropertyBinder)DependencyResolver.Current.GetService(propertyBinderAttribute.BinderType); 
     } 

     PropertyBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor) 
     { 
      return propertyDescriptor.Attributes 
       .OfType<PropertyBinderAttribute>() 
       .FirstOrDefault(); 
     } 
    } 
} 

mio IPropertyBinder interfaccia:

namespace MyApp.Web.Mvc 
{ 
    interface IPropertyBinder 
    { 
     object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, MemberDescriptor memberDescriptor); 
    } 
} 

mio PropertyBinderAttribute:

namespace MyApp.Web.Mvc 
{ 
    public class PropertyBinderAttribute : Attribute 
    { 
     public PropertyBinderAttribute(Type binderType) 
     { 
      BinderType = binderType; 
     } 

     public Type BinderType { get; private set; } 
    } 
} 

Un esempio di un legante struttura:

namespace MyApp.Web.Mvc.PropertyBinders 
{ 
    public class TimeSpanBinder : IPropertyBinder 
    { 
     readonly HttpContextBase _httpContext; 

     public TimeSpanBinder(HttpContextBase httpContext) 
     { 
      _httpContext = httpContext; 
     } 

     public object BindModel(
      ControllerContext controllerContext, 
      ModelBindingContext bindingContext, 
      MemberDescriptor memberDescriptor) 
     { 
      var timeString = _httpContext.Request.Form[memberDescriptor.Name].ToLower(); 
      var timeParts = timeString.Replace("am", "").Replace("pm", "").Trim().Split(':'); 
      return 
       new TimeSpan(
        int.Parse(timeParts[0]) + (timeString.Contains("pm") ? 12 : 0), 
        int.Parse(timeParts[1]), 
        0); 
     } 
    } 
} 

Esempio di sopra legante proprietà utilizzata:

namespace MyApp.Web.Models 
{ 
    public class MyModel 
    { 
     [PropertyBinder(typeof(TimeSpanBinder))] 
     public TimeSpan InspectionDate { get; set; } 
    } 
} 
+1

Suppongo che questo approccio possa essere migliorato sostituendo 'GetPropertyValue'. Vedi [la mia risposta] (http://stackoverflow.com/a/19727630/129073) per i dettagli. – Gebb

+0

È possibile avere un'implementazione di PropertyBinder simile per l'API Web? – mehul9595

+2

'CreateBinder' non funziona con questo costruttore, quindi ho abbandonato' HttpContext' e ho usato 'var value = bindingContext.ValueProvider.GetValue (memberDescriptor.Name);' per ottenere il valore della proprietà. – Brad

5

@ risposta di jonathanconway è grande, ma vorrei aggiungere un piccolo dettaglio.

Probabilmente è meglio sostituire il metodo GetPropertyValue anziché BindProperty per dare al meccanismo di convalida dello DefaultBinder la possibilità di funzionare.

protected override object GetPropertyValue(
    ControllerContext controllerContext, 
    ModelBindingContext bindingContext, 
    PropertyDescriptor propertyDescriptor, 
    IModelBinder propertyBinder) 
{ 
    PropertyBinderAttribute propertyBinderAttribute = 
     TryFindPropertyBinderAttribute(propertyDescriptor); 
    if (propertyBinderAttribute != null) 
    { 
     propertyBinder = CreateBinder(propertyBinderAttribute); 
    } 

    return base.GetPropertyValue(
     controllerContext, 
     bindingContext, 
     propertyDescriptor, 
     propertyBinder); 
} 
+1

A seconda dell'utilizzo desiderato, questo potrebbe funzionare per voi e potrebbe non farlo.Tenere presente che GetPropertyValue si attiva solo quando un elemento nell'oggetto in entrata (in questo caso, Modulo di richiesta/stringa di query) corrisponde alla proprietà. Se volessi un raccoglitore di modelli personalizzato che fosse in grado di cercare elementi con un nome diverso, ad esempio forse per popolare un dizionario, non saresti fortunato. Ad esempio, mappando "field1 = test & field2 = ing & field3 = now" verrebbe mappato a un dizionario in cui "1" era la chiave e "test" era il valore. La soluzione di Jonathan Conway lo consente. Forse un ibrido, permettendo a entrambi è meglio. –

1

Sono passati 6 anni da quando questa domanda è stato chiesto, avrei preferito prendere questo spazio per riassumere l'aggiornamento, invece di fornire una nuova soluzione. Al momento della scrittura, MVC 5 è in circolazione da un po 'di tempo e ASP.NET Core è appena uscito.

Ho seguito l'approccio esaminato nel post scritto da Vijaya Anand (btw, grazie a Vijaya): http://www.prideparrot.com/blog/archive/2012/6/customizing_property_binding_through_attributes. E una cosa che vale la pena notare è che la logica di associazione dei dati è collocata nella classe di attributi personalizzata, che è il metodo BindProperty della classe StringArrayPropertyBindAttribute nell'esempio di Vijaya Anand.

Tuttavia, in tutti gli altri articoli su questo argomento che ho letto (inclusa la soluzione di @ jonathanconway), la classe di attributo personalizzata è solo una pietra miliare che guida il framework per trovare il corretto modello di raccoglitore personalizzato da applicare; e la logica di collegamento viene inserita in tale raccoglitore modello personalizzato, che di solito è un oggetto IModelBinder.

Il primo approccio è più semplice per me. Potrebbero esserci alcune carenze del primo approccio, che non ho ancora conosciuto, tuttavia, al momento sono abbastanza nuovo nel framework MVC.

Inoltre, ho trovato che la classe ExtendedModelBinder nell'esempio di Vijaya Anand non è necessaria in MVC 5. Sembra che la classe DefaultModelBinder fornita con MVC 5 sia abbastanza intelligente da cooperare con gli attributi di associazione modello personalizzati.

Problemi correlati