2010-02-15 18 views
14

Questa domanda è stata ispirata dalle mie difficoltà con ASP.NET MVC, ma penso che si applichi anche ad altre situazioni.Come "DRY up" gli attributi C# in Models e ViewModels?

Diciamo che ho un modello ORM-generated e due ViewModels (uno per una vista "Dettagli" e uno per una "modifica" vista):

Modello

public class FooModel // ORM generated 
{ 
    public int Id { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public string EmailAddress { get; set; } 
    public int Age { get; set; } 
    public int CategoryId { get; set; } 
} 

display ViewModel

public class FooDisplayViewModel // use for "details" view 
{ 
    [DisplayName("ID Number")] 
    public int Id { get; set; } 

    [DisplayName("First Name")] 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] 
    [DataType("EmailAddress")] 
    public string EmailAddress { get; set; } 

    public int Age { get; set; } 

    [DisplayName("Category")] 
    public string CategoryName { get; set; } 
} 

Modifica ViewModel

public class FooEditViewModel // use for "edit" view 
{ 
    [DisplayName("First Name")] // not DRY 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] // not DRY 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] // not DRY 
    [DataType("EmailAddress")] // not DRY 
    public string EmailAddress { get; set; } 

    public int Age { get; set; } 

    [DisplayName("Category")] // not DRY 
    public SelectList Categories { get; set; } 
} 

Si noti che gli attributi sui ViewModels non sono a secco - un sacco di informazioni viene ripetuta. Ora immagina questo scenario moltiplicato per 10 o 100, e puoi vedere che può diventare rapidamente noioso e soggetto a errori per assicurare la coerenza tra ViewModels (e quindi attraverso Views).

Come è possibile "ASCIUGARE" questo codice?

Prima di rispondere, "Basta mettere tutti gli attributi su FooModel," Ho provato, ma non ha funzionato perché ho bisogno di mantenere il mio ViewModels "flat". In altre parole, non posso semplicemente comporre ogni ViewModel con un modello - Ho bisogno del mio ViewModel per avere solo le proprietà (e gli attributi) che dovrebbero essere consumati dalla vista, e la vista non può scavare in sotto-proprietà per ottenere i valori.

Aggiornamento

risposta di LukLed suggerisce di usare l'ereditarietà. Ciò riduce decisamente la quantità di codice non DRY, ma non lo elimina. Si noti che, nel mio esempio sopra, l'attributo DisplayName per la proprietà Category dovrebbe essere scritto due volte perché il tipo di dati della proprietà è diverso tra la visualizzazione e modifica ViewModels. Questo non sarà un grosso problema su piccola scala, ma man mano che le dimensioni e la complessità di un progetto si ridimensionano (immaginate molte più proprietà, più attributi per proprietà, più visualizzazioni per modello), c'è ancora il potenziale per "ripetendo te stesso" una discreta quantità. Forse sto prendendo DRY troppo lontano qui, ma preferirei avere tutti i miei "nomi amichevoli", i tipi di dati, le regole di convalida, ecc., Digitato una sola volta.

risposta

7

Suppongo che tu lo stia facendo per sfruttare HtmlHelpers EditorFor e DisplayFor e non vuoi che il sovraccarico di certificare cerimoniosamente la stessa cosa 4000 volte in tutta l'applicazione.

Il modo più semplice per ASCIUGARLA è implementare il proprio ModelMetadataProvider.ModelMetadataProvider è ciò che legge questi attributi e li presenta agli helper dei modelli. MVC2 fornisce già un'implementazione DataAnnotationsModelMetadataProvider per far sì che le cose procedano in modo tale da rendere le cose davvero facili.

Per iniziare ecco un semplice esempio che rompe i nomi di proprietà a parte formato camelCase in spazi, Nome => Nome:

public class ConventionModelMetadataProvider : DataAnnotationsModelMetadataProvider 
{ 
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) 
    { 
     var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); 

     HumanizePropertyNamesAsDisplayName(metadata); 

     if (metadata.DisplayName.ToUpper() == "ID") 
      metadata.DisplayName = "Id Number"; 

     return metadata; 
    } 

    private void HumanizePropertyNamesAsDisplayName(ModelMetadata metadata) 
    { 
     metadata.DisplayName = HumanizeCamel((metadata.DisplayName ?? metadata.PropertyName)); 
    } 

    public static string HumanizeCamel(string camelCasedString) 
    { 
     if (camelCasedString == null) 
      return ""; 

     StringBuilder sb = new StringBuilder(); 

     char last = char.MinValue; 
     foreach (char c in camelCasedString) 
     { 
      if (char.IsLower(last) && char.IsUpper(c)) 
      { 
       sb.Append(' '); 
      } 
      sb.Append(c); 
      last = c; 
     } 
     return sb.ToString(); 
    } 
} 

Poi tutto quello che dovete fare è registrarlo come l'aggiunta tue ViewEngine o ControllerFactory all'interno di Avvio applicazione di Global.asax:

ModelMetadataProviders.Current = new ConventionModelMetadataProvider(); 

Ora solo per mostrarvi non sto barare questo è il modello di vista che sto usando per ottenere lo stesso HtmlHelper * per esperienza come la vostra decorato ViewModel.. :

public class FooDisplayViewModel // use for "details" view 
    { 
     public int Id { get; set; } 

     public string FirstName { get; set; } 

     public string LastName { get; set; } 

     [DataType("EmailAddress")] 
     public string EmailAddress { get; set; } 

     public int Age { get; set; } 

     [DisplayName("Category")] 
     public string CategoryName { get; set; } 
    } 
+0

Grazie, jfar e +1. Sì, esattamente, sto cercando di usare DisplayFor() e EditorFor() (anche se, anche nei casi in cui non posso, mi piacerebbe ancora ASCIUGARE i miei ViewModels). La tua idea rimuoverebbe molto bisogno di attributi, il che è di grande aiuto. Mi chiedo, tuttavia, se potessi aggiungere anche una proprietà personalizzata (e un attributo personalizzato parallelo) che indicherebbe se impalcare una particolare proprietà per un particolare ViewModel. Questo mi permetterebbe di avere un ViewModel che gestisce tutte le viste, il che significa che non avrei mai o quasi mai bisogno di ripetere gli attributi. – devuxer

+0

L'unica limitazione è rappresentata dalle proprietà predefinite di ModelMetadata. Se hai bisogno di aggiungere ulteriori informazioni e creare un MyModelMetadata: ModelMetatdata avrai anche creato la tua ViewPage personalizzata con una proprietà MyModelMetadata personalizzata OPPURE lanciare ViewData.ModelMetadata all'interno di qualsiasi file .aspx o .ascx che usi. – jfar

7

Declare BaseModel, ereditare e aggiungere un altro proprietà:

public class BaseFooViewModel 
{ 
    [DisplayName("First Name")] 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] 
    [DataType("EmailAddress")] 
    public string EmailAddress { get; set; } 
} 

public class FooDisplayViewModel : BaseFooViewModel 
{ 
    [DisplayName("ID Number")] 
    public int Id { get; set; } 
} 

public class FooEditViewModel : BaseFooViewModel 

EDIT

Informazioni sulle categorie. Non dovresti modificare il modello di visualizzazione con public string CategoryName { get; set; } e public List<string> Categories { get; set; } invece di SelectList? In questo modo è possibile inserire public string CategoryName { get; set; } nella classe base e mantenere DRY. La visualizzazione Modifica migliora la classe aggiungendo List<string>.

+0

Ho giocato prima con l'ereditarietà. Ti avvicina di certo, ma manca di flessibilità. Un problema è che alcuni attributi devono ancora essere ripetuti, come l'attributo 'DisplayName' per' Category' nel mio esempio. Ovviamente, questo non è un grosso problema su piccola scala, ma su larga scala, potreste finire con un sacco di voci doppie o triple dello stesso 'DisplayName' per diversi ViewModels (e questo è solo uno dei numerosi attributi che potreste vuoi impostare per una determinata proprietà). – devuxer

+0

@DanM: ho aggiunto commenti su Category. – LukLed

+0

@LukLed, non sono sicuro di seguirti. Sto usando 'SelectList' perché, quando la vista è impalcata, un' DropDownList' verrà creato con l'elenco corretto e l'elemento inizialmente selezionato. Posso anche collegare 'DataValueField' e' DataTextField' per i casi in cui l'elenco è in realtà una tabella di database. Se uso semplicemente un 'Elenco ', non credo che il mio punto di vista sia impalcato correttamente. – devuxer

1

Come ha detto LukLed, è possibile creare una classe base da cui derivano i modelli Visualizza e Modifica oppure è possibile derivare anche un modello di visualizzazione dall'altro. In molte app il modello Edit è fondamentalmente uguale a View e alcune cose aggiuntive (come gli elenchi selezionati), quindi potrebbe avere senso derivare il modello Edit dal modello View.

Oppure, se si è preoccupati di "esplosione di classe", è possibile utilizzare lo stesso modello di visualizzazione per entrambi e passare gli elementi aggiuntivi (come le liste di selezione) tramite ViewData. Non consiglio questo approccio perché penso che sia confuso passare qualche stato tramite il Modello e altri stati tramite ViewData, ma è un'opzione.

Un'altra opzione sarebbe semplicemente abbracciare i modelli separati. Sto tutto nel mantenere la logica DRY, ma sono meno preoccupato di alcune proprietà ridondanti nei miei DTO (specialmente nei progetti che usano la generazione del codice per generare il 90% dei modelli di visualizzazione per me).

1

Per prima cosa ho notato - hai 2 modelli di vista. Vedere la mia risposta here per i dettagli su questo.

Altre cose che vengono in mente sono già menzionate (approccio classico per applicare DRY - ereditarietà e convenzioni).


Credo di essere stato troppo vago. La mia idea è di creare il modello di visualizzazione per modello di dominio e quindi - combinarli a modelli di vista che sono per visualizzazione specifica. Nel tuo caso: =>

public class FooViewModel { 
    strange attributes everywhere tralalala 
    firstname,lastname,bar,fizz,buzz 
} 

public class FooDetailsViewModel { 
    public FooViewModel Foo {get;set;} 
    some additional bull**** if needed 
} 

public class FooEditViewModel { 
    public FooViewModel Foo {get;set;} 
    some additional bull**** if needed 
} 

Questo ci permette di creare modelli di visione più complessa (che sono per view) troppo =>

public class ComplexViewModel { 
    public PaginationInfo Pagination {get;set;} 
    public FooViewModel Foo {get;set;} 
    public BarViewModel Bar {get;set;} 
    public HttpContext lol {get;set;} 
} 

Si potrebbe trovare utile this question di mine.

hmm ... risulta che in realtà ho suggerito di creare 3 modelli di vista. Ad ogni modo, quel frammento di codice gentile rispecchia il mio approccio.

Un altro suggerimento - vorrei andare con il filtro & convenzione (ad esempio per tipo) meccanismo che riempie viewdata con selectList necessario (framework mvc può associare automaticamente selectList da viewData per nome o qualcosa).

E un altro consiglio: se si utilizza AutoMapper per la gestione del modello di visualizzazione, ha una bella funzionalità: può essere flatten object graph.Quindi - puoi creare il modello di vista (che è per visualizzazione) che ha direttamente oggetti di scena del modello di vista (che è per modello di dominio) qualunque sia la profondità che vuoi andare (Haack said va bene).

+0

Grazie, Arnis. Sembra che tu stia dicendo che dovrei avere un * terzo * modello di vista (ad esempio, 'FooEditPostViewModel'). In realtà ho pensato di farlo in vari momenti, ma esacerba solo i problemi che sto avendo con gli attributi. Sono affascinato dal Fluent MetadataProvider, però. Lo esamineremo. – devuxer

+0

@DanM ha aggiornato un po 'la mia risposta. –

+0

Arnis, grazie per i dettagli extra. Una cosa che mi interessa fare è l'auto-impalcatura (usando 'DisplayFor()' e 'EditorFor()'). Sia che io usi la composizione o l'ereditarietà, le cose finiscono nell'ordine sbagliato o appaiono come indentate quando non dovrebbero. – devuxer

0

Questi nomi visualizzati (i valori) potrebbero essere visualizzati in un'altra classe statica con molti campi const. Non ti farebbe risparmiare molte istanze di DisplayNameAttribute, ma renderebbe un cambio di nome rapido e facile da fare. Ovviamente questo non è utile per altri meta attributi.

Se avessi detto al mio team che avrebbero dovuto creare un nuovo modello per ogni piccola permutazione degli stessi dati (e successivamente scrivere definizioni dell'autore per loro) si sarebbero ribellati e linciami. Preferirei modellare i metadati che erano, anche in qualche modo, consapevoli dell'uso. Ad esempio, l'attribuzione di un attributo delle proprietà ha effetto solo in uno scenario "Aggiungi" (Modello == null). Soprattutto perché non scriverei nemmeno due viste per gestire l'aggiunta/modifica. Avrei una vista per gestirli entrambi e se avessi iniziato ad avere diverse classi di modelli avrei avuto problemi con la mia dichiarazione della classe genitore ... il bit di ViewPage.

Problemi correlati