Domanda Asp.Net MVC 2 - Associare la proprietà di un modello a un valore con nome diverso


Aggiornamento (21 settembre 2016)  - Grazie a Digbyswift per aver commentato che questa soluzione funziona ancora anche in MVC5.

Aggiornamento (30 aprile 2012)  - Nota per le persone che incappano in questa domanda dalle ricerche ecc. - la risposta accettata non è come ho finito per farlo, ma l'ho lasciata accettata perché potrebbe aver funzionato in alcuni casi. La mia risposta contiene la soluzione finale che ho usato, che è riutilizzabile e si applicherà a qualsiasi progetto.

È anche confermato di funzionare in v3 e v4 del framework MVC.

Ho il seguente tipo di modello (i nomi della classe e le sue proprietà sono stati modificati per proteggere le loro identità):

public class MyExampleModel
{
  public string[] LongPropertyName { get; set; }
}

Questa proprietà viene quindi associata a un gruppo (> 150) di caselle di controllo, in cui il nome di input di ciascuno è ovviamente LongPropertyName.

Il modulo si invia all'URL con un HTTP GET e dice che l'utente seleziona tre di queste caselle di controllo: l'url avrà la stringa di query ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c

Il grosso problema è che se seleziono tutte (o anche solo più della metà!) Le caselle di controllo, eccedo la lunghezza massima della stringa di query applicata dal filtro delle richieste su IIS!

Non voglio estendere questo - quindi voglio un modo per ridurre questa stringa di query (so che posso passare a un POST - ma anche così voglio ancora minimizzare la quantità di fluff nei dati inviati dal client) .

Quello che voglio fare è avere il LongPropertyName vincolato semplicemente a "L" così diventerebbe la stringa di query ?L=a&L=b&L=c ma senza cambiare il nome della proprietà nel codice.

Il tipo in questione ha già un modello personalizzato (derivato da DefaultModelBinder), ma è collegato alla sua classe base, quindi non voglio inserire il codice per una classe derivata. Tutti i binding di proprietà sono attualmente eseguiti dalla logica standard DefaultModelBinder, che so utilizza TypeDescriptors e Property Descriptors ecc. Da System.ComponentModel.

Speravo che ci potesse essere un attributo che potrei applicare alla proprietà per far funzionare questo - c'è? O dovrei guardare all'attuazione ICustomTypeDescriptor?


44
2017-11-30 16:55


origine


risposte:


Puoi usare il BindAttribute per realizzare questo.

public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {

}

Aggiornare

Poiché il parametro 'longPropertyName' fa parte dell'oggetto modello e non è un parametro indipendente dell'azione del controller, hai un paio di altre scelte.

È possibile mantenere il modello e la proprietà come parametri indipendenti per l'azione e quindi unire manualmente i dati insieme nel metodo di azione.

public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
    if(myModel != null) {
        myModel.LongPropertyName = longPropertyName;
    }
}

Un'altra opzione sarebbe l'implementazione di un Raccoglitore modello personalizzato che esegue manualmente l'assegnazione del valore dei parametri (come sopra), ma è probabile che si tratti di overkill. Ecco un esempio di uno, se sei interessato: Binder del modello di enumerazione delle bandiere.


18
2017-11-30 16:58



In risposta alla risposta e alla richiesta di michaelalm, ecco cosa ho finito per fare. Ho lasciato la risposta originale segnata principalmente per cortesia dal momento che una delle soluzioni suggerite da Nathan avrebbe funzionato.

L'output di questo è un rimpiazzo per DefaultModelBinder classe che puoi registrare globalmente (consentendo così a tutti i tipi di modelli di sfruttare l'aliasing) o ereditare selettivamente per i raccoglitori di modelli personalizzati.

Tutto inizia, prevedibilmente con:

/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
/// 
/// The type needs to be using the DefaultModelBinderEx model binder in 
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
  public BindAliasAttribute(string alias)
  {
    //ommitted: parameter checking
    Alias = alias;
  }
  public string Alias { get; private set; }
}

E poi prendiamo questa classe:

internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
  public PropertyDescriptor Inner { get; private set; }

  public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
    : base(alias, null)
  {
    Inner = inner;
  }

  public override bool CanResetValue(object component)
  {
    return Inner.CanResetValue(component);
  }

  public override Type ComponentType
  {
    get { return Inner.ComponentType; }
  }

  public override object GetValue(object component)
  {
    return Inner.GetValue(component);
  }

  public override bool IsReadOnly
  {
    get { return Inner.IsReadOnly; }
  }

  public override Type PropertyType
  {
    get { return Inner.PropertyType; }
  }

  public override void ResetValue(object component)
  {
    Inner.ResetValue(component);
  }

  public override void SetValue(object component, object value)
  {
    Inner.SetValue(component, value);
  }

  public override bool ShouldSerializeValue(object component)
  {
    return Inner.ShouldSerializeValue(component);
  }
}

Questo proxy un PropertyDescriptor 'appropriato' che viene normalmente trovato da DefaultModelBinder ma presenta il suo nome come alias.

Successivamente abbiamo la nuova classe di rilegatore del modello:

public class DefaultModelBinderEx : DefaultModelBinder
{
  protected override System.ComponentModel.PropertyDescriptorCollection
    GetModelProperties(ControllerContext controllerContext, 
                      ModelBindingContext bindingContext)
  {
    var toReturn = base.GetModelProperties(controllerContext, bindingContext);

    List<PropertyDescriptor> additional = new List<PropertyDescriptor>();

    //now look for any aliasable properties in here
    foreach (var p in 
      this.GetTypeDescriptor(controllerContext, bindingContext)
      .GetProperties().Cast<PropertyDescriptor>())
    {
      foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
      {
        additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));

        if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
          bindingContext.PropertyMetadata.Add(attr.Alias,
                bindingContext.PropertyMetadata[p.Name]);
      }
    }

    return new PropertyDescriptorCollection
      (toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
  }
}

E, quindi tecnicamente, non c'è altro da fare. Ora puoi registrarlo DefaultModelBinderEx class come default usando la soluzione pubblicata come risposta in questo SO: Modificare il modello di associazione predefinito in asp.net MVCoppure puoi usarlo come base per il tuo raccoglitore di modelli.

Una volta selezionato il motivo per il modo in cui desideri che il raccoglitore entri in gioco, devi semplicemente applicarlo a un tipo di modello come segue:

public class TestModelType
{
    [BindAlias("LPN")]
    //and you can add multiple aliases
    [BindAlias("L")]
    //.. ad infinitum
    public string LongPropertyName { get; set; }
}

Il motivo per cui ho scelto questo codice era perché volevo qualcosa che avrebbe funzionato con descrittori di tipi personalizzati oltre a poter lavorare con qualsiasi tipo. Allo stesso modo, volevo che il sistema del value provider fosse ancora utilizzato per l'approvvigionamento dei valori delle proprietà del modello. Quindi ho cambiato i metadati che il DefaultModelBinder vede quando inizia a legare. È un approccio un po 'più prolisso - ma concettualmente sta facendo a livello di metadati esattamente quello che vuoi che faccia.

Un effetto collaterale potenzialmente interessante e leggermente fastidioso sarà se il ValueProvider contiene valori per più di un alias o un alias e la proprietà per il suo nome. In questo caso, verrà utilizzato solo uno dei valori recuperati. È difficile pensare a un modo per unirli tutti in un modo sicuro per i tipi quando lavori objects però. Ciò è simile, tuttavia, a fornire un valore sia in un post di modulo che in una stringa di query e non sono sicuro di ciò che MVC fa in tale scenario, ma non penso che sia una pratica consigliata.

Un altro problema è, ovviamente, che non è necessario creare un alias uguale a un altro alias o al nome di una proprietà effettiva.

Mi piace applicare i miei legatori di modelli, in generale, usando il CustomModelBinderAttribute classe. L'unico problema con questo può essere se è necessario derivare dal tipo di modello e modificarne il comportamento vincolante, dal momento che CustomModelBinderAttribute è ereditato dalla ricerca degli attributi eseguita da MVC.

Nel mio caso va bene, sto sviluppando una nuova struttura del sito e sono in grado di spingere la nuova estensibilità nei miei raccoglitori di base usando altri meccanismi per soddisfare questi nuovi tipi; ma non sarà così per tutti.


78
2017-11-30 16:58



questa sarebbe una soluzione simile alla tua Andras? spero che tu possa postare anche la tua risposta.

metodo del controller

public class MyPropertyBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);

        for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
        {
            if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
            {                    
                // set property value.
                propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
                break;
            }
        }
    }
}

Attributo

public class BindingNameAttribute : Attribute
{
    public string Name { get; set; }

    public BindingNameAttribute()
    {

    }
}

ViewModel

public class EmployeeViewModel
{                    

    [BindingName(Name = "txtName")]
    public string TestProperty
    {
        get;
        set;
    }
}

quindi utilizzare il Raccoglitore nel controller

[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
        // do stuff here
}

il valore del modulo txtName deve essere impostato su TestProperty.


4
2018-01-18 03:27