2010-02-15 4 views
14

Diese Frage wurde von meinen Kämpfen mit ASP.NET MVC inspiriert, aber ich denke, es gilt auch für andere Situationen.Wie "C# Attribute" in Models und ViewModels "DRY-up"?

Lassen Sie uns sagen, ich habe ein ORM-generierte Modell und zwei Viewmodels (eine für eine Ansicht "Details" und eine für ein "Bearbeiten" -Ansicht):

Modell

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; } 
} 

Anzeige Ansichtsmodell

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; } 
} 

bearbeiten Ansichtsmodell

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; } 
} 

Beachten Sie, dass die Attribute auf den Viewmodels sind nicht DRY - eine Menge an Informationen wird wiederholt. Stellen Sie sich nun vor, dass dieses Szenario mit 10 oder 100 multipliziert wird, und Sie können sehen, dass es schnell ziemlich mühsam und fehleranfällig werden kann, die Konsistenz zwischen ViewModels (und damit zwischen den Views) zu gewährleisten.

Wie kann ich diesen Code "DRY-up"?

Bevor Sie antworten, "setzen Sie einfach alle Attribute auf FooModel," Ich habe das versucht, aber es hat nicht funktioniert, weil ich meine ViewModels "flach" halten muss. Mit anderen Worten, ich kann nicht jedes ViewModel mit einem Model zusammenstellen - ich brauche mein ViewModel, um nur die Eigenschaften (und Attribute) zu haben, die von der View konsumiert werden sollen, und die View kann nicht in Untereigenschaften einbetten Auf die Werte kommen.

aktualisieren

Antwort LukLed lässt darauf schließen Vererbung. Dies reduziert definitiv die Menge an nicht-DRY-Code, aber es beseitigt es nicht. Beachten Sie, dass in meinem obigen Beispiel das DisplayName-Attribut für die Eigenschaft Category zweimal geschrieben werden muss, da der Datentyp der Eigenschaft zwischen den Anzeige- und Bearbeitungsansichtsmodellen unterschiedlich ist. Dies wird im kleinen Maßstab keine große Sache sein, aber wenn die Größe und Komplexität eines Projekts größer wird (man stelle sich viel mehr Eigenschaften, mehr Attribute pro Eigenschaft, mehr Ansichten pro Modell vor), gibt es immer noch das potentiell "Sich selbst wiederholen" eine angemessene Menge. Vielleicht nehme ich DRY hier zu weit, aber ich würde immer noch lieber alle meine "freundlichen Namen", Datentypen, Validierungsregeln usw. nur einmal eingeben.

Antwort

7

Ich gehe davon aus, dass Sie dies tun, um die HtmlHelpers EditorFor und DisplayFor nutzen und nicht wollen, dass der Overhead in der gesamten Anwendung zeremonielle dasselbe 4000 Mal erklärt.

Der einfachste Weg, um dies zu trocknen ist die Implementierung eines eigenen ModelMetadataProvider.Der ModelMetadataProvider liest diese Attribute und präsentiert sie den Vorlagenhelfern. MVC2 stellt bereits eine DataAnnotationsModelMetadataProvider-Implementierung bereit, um Dinge in Gang zu bringen, von denen das Erben sehr einfach ist.

Sie hier, um loszulegen ist ein einfaches Beispiel, das abgesehen camelcase Eigenschaftsnamen in Räume bricht, Vorname => Vorname:

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(); 
    } 
} 

Dann alles, was Sie tun müssen, ist es zu registrieren, wie Sie Ihre eigene individuelle Zugabe von Viewengine oder Controller innerhalb von Global.asax Application Start:

ModelMetadataProviders.Current = new ConventionModelMetadataProvider(); 

Jetzt nur um zu zeigen Sie, ich bin nicht das Betrug ist die Ansicht Modell ich bin mit dem gleichen Htmlhelper erhalten * Für Erfahrung als dekoriert Ansichtsmodell.. :

+0

Danke, jfar und +1. Ja, genau, ich versuche, DisplayFor() und EditorFor() zu verwenden (obwohl ich selbst in Fällen, in denen ich das nicht kann, meine ViewModels immer noch DRY). Ihre Idee würde viele Attribute überflüssig machen, was eine große Hilfe ist. Ich frage mich allerdings, ob ich auch eine benutzerdefinierte Eigenschaft (und ein paralleles benutzerdefiniertes Attribut) hinzufügen könnte, die angeben würde, ob eine bestimmte Eigenschaft für ein bestimmtes ViewModel gerastert werden soll. Dies würde mir erlauben, ein ViewModel zu haben, das alle Ansichten behandelt, was bedeutet, dass ich nie oder fast nie Attribute wiederholen müsste. – devuxer

+0

Nun, die einzige Einschränkung sind die Standardeigenschaften von ModelMetadata. Wenn Sie weitere Informationen hinzufügen und ein MyModelMetadata erstellen müssen: ModelMetatdata, müssen Sie auch ein eigenes benutzerdefiniertes ViewPage mit einer benutzerdefinierten MyModelMetadata-Eigenschaft erstellen ODER das ViewData.ModelMetadata in beliebige ASPX- oder ASCX-Dateien umwandeln, die Sie verwenden. – jfar

7

Declare Basemodel, erben und fügen Sie weitere Eigenschaften:

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

Über Kategorien. Soll das View-Modell nicht bearbeitet werden, haben public string CategoryName { get; set; } und public List<string> Categories { get; set; } anstelle von SelectList? Auf diese Weise können Sie public string CategoryName { get; set; } in der Basisklasse platzieren und DRY behalten. Die Bearbeitungsansicht erweitert die Klasse um List<string>.

+0

Ich habe schon mit Vererbung gespielt. Es bringt dich definitiv näher, aber es fehlt an Flexibilität. Ein Problem besteht darin, dass einige Attribute noch wiederholt werden müssen, wie das Attribut "DisplayName" für "Category" in meinem Beispiel. Natürlich ist dies im kleinen Maßstab keine große Sache, aber in großem Umfang könnte man viele doppelte oder dreifache Einträge desselben 'DisplayName' für verschiedene ViewModels erhalten (und das ist nur eines von mehreren Attributen, die Sie vielleicht haben) möchte für eine bestimmte Eigenschaft festlegen). – devuxer

+0

@DanM: Ich habe einen Kommentar über Kategorie hinzugefügt. – LukLed

+0

@LukLed, ich bin mir nicht sicher, ob ich dir folge. Ich benutze 'SelectList', weil, wenn die Ansicht gerüstartig ist, eine' DropDownList' mit der richtigen Liste und dem ursprünglich ausgewählten Element erstellt wird. Ich kann sogar 'DataValueField' und' DataTextField' verbinden, wenn die Liste tatsächlich eine Datenbanktabelle ist. Wenn ich nur eine 'List ' verwende, glaube ich nicht, dass meine Ansicht korrekt gerüstet wäre. – devuxer

1

Wie LukLed sagte, könnten Sie eine Basisklasse erstellen, von der die View- und Edit-Modelle abgeleitet sind, oder Sie könnten auch nur ein View-Modell von dem anderen ableiten. In vielen Apps ist das Edit-Modell im Grunde dasselbe wie View plus einige zusätzliche Dinge (wie Auswahllisten), so dass es sinnvoll sein könnte, das Edit-Modell aus dem View-Modell abzuleiten.

Oder, wenn Sie über "Klassenexplosion" besorgt sind, könnten Sie das gleiche Ansichtsmodell für beide verwenden und die zusätzlichen Sachen (wie SelectLists) durch ViewData übergeben. Ich empfehle diesen Ansatz nicht, weil ich denke, es ist verwirrend, über ViewData einige Status über das Modell und anderen Zustand zu übergeben, aber es ist eine Option.

Eine andere Option wäre, die einzelnen Modelle einfach zu umfassen. Mir geht es darum, Logik-DRY zu behalten, aber ich mache mir weniger Sorgen um ein paar redundante Eigenschaften in meinen DTOs (besonders bei Projekten, die Code-Generierung verwenden, um 90% der View-Modelle für mich zu generieren).

1

Das erste, was ich bemerke - Sie haben 2 Ansicht Modelle. Siehe meine Antwort here für Details dazu.

Andere Dinge, die im Sinn sind, sind bereits erwähnt (klassischer Ansatz zur Anwendung von DRY - Vererbung und Konventionen).


Ich denke, ich war zu vage. Meine Idee ist es, View-Modell pro Domain-Modell zu erstellen und dann - kombinieren Sie sie in Ansichtsmodelle, die pro bestimmte Ansicht sind. In Ihrem Fall: =>

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 
} 

Das führt uns komplexere Sicht Modelle erstellen können (die pro Ansicht) sind zu =>

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

Sie könnten nützlich this question von mir finden.

hmm ... stellt sich heraus, ich habe tatsächlich vorgeschlagen, 3 Ansicht Modelle zu erstellen. Wie auch immer, dieses Code-Snippet spiegelt meinen Ansatz wider.

Noch ein Tipp - i mit Filter & Konvention (zum Beispiel nach Typ) gehen würde, basierend Mechanismus, das Bildschirmtextsystem mit notwendigen select (MVC-Framework mit Namen oder etwas kann automagically binden select von Bildschirmtextsystem) füllt.

Und noch ein Tipp - wenn Sie AutoMapper für die Verwaltung Ihrer Ansicht Modell verwenden, hat es eine nette Funktion - es kann flatten object graph.Daher - Sie können View-Modell erstellen (was pro Ansicht ist), die direkt Requisiten des Ansichtsmodells hat (was pro Domain-Modell ist), egal wie tief Sie gehen möchten (Haack said ist in Ordnung).

+0

Danke, Arnis. Es klingt wie Sie sagen, dass ich ein * drittes * Ansichtsmodell haben sollte (z. B. 'FooEditPostViewModel'). Ich habe tatsächlich darüber nachgedacht, das zu verschiedenen Zeiten zu tun, aber es verschärft nur die Probleme, die ich mit Attributen habe. Ich bin jedoch vom Fluent MetadataProvider fasziniert. Werde das untersuchen. – devuxer

+0

@DanM aktualisiert ein bisschen meine Antwort. –

+0

Arnis, danke für das extra Detail. Eine Sache, an der ich interessiert bin, ist Auto-Scaffolding (mit 'DisplayFor()' und 'EditorFor()'). Egal, ob ich Komposition oder Vererbung verwende, die Dinge landen in der falschen Reihenfolge oder werden als eingerückt angezeigt, wenn sie nicht sollten. – devuxer

0

Diese Anzeigenamen (die Werte) könnten möglicherweise in einer anderen statischen Klasse mit vielen const-Feldern angezeigt werden. Würden Sie nicht viele Instanzen von DisplayNameAttribute speichern, aber das würde eine Namensänderung schnell und einfach machen. Offensichtlich ist dies für andere Metaattribute nicht hilfreich.

Wenn ich meinem Team sagte, dass sie ein neues Modell für jede kleine Permutation der gleichen Daten erstellen müssten (und anschließend Auto-Mapper-Definitionen für sie schreiben würden), würden sie mich rebellieren und lynchen. Ich würde eher Metadaten modellieren, die zu einem gewissen Grad bewusst waren. Beispielsweise wird das Attribut Eigenschaften erforderlich nur in einem Szenario "Hinzufügen" (Modell == null) wirksam. Zumal ich nicht mal zwei Ansichten schreiben würde, um mit add/edit umzugehen. Ich würde eine Ansicht haben, um mit beiden umzugehen, und wenn ich anfing, verschiedene Modellklassen zu haben, würde ich in Schwierigkeiten mit meiner Elternklasse-Deklaration kommen .. das ... ViewPage Bit.