2011-01-17 5 views
8

Kurz gesagt ich mag in der Lage sein, eine generische Ansichtsmodell in meine Ansichten passierenMVC generisches Ansichtsmodell

Hier einige vereinfachte Code des Kerns ist, was ich versuche

public interface IPerson 
{ 
    string FirstName {get;} 
    string LastName {get;} 
} 

public class FakePerson : IPerson 
{ 
    public FakePerson() 
    { 
     FirstName = "Foo"; 
     LastName = "Bar"; 
    } 

    public string FirstName {get; private set;} 
    public string LastName {get; private set;} 
} 

public class HomeViewModel<T> 
    where T : IPerson, new() 
{ 
    public string SomeOtherProperty{ get; set;} 
    public T Person { get; private set; } 

    public HomeViewModel() 
    { 
     Person = new T(); 
    } 
} 

public class HomeController : Controller { 
    public ViewResult Index() { 
     return View(new HomeViewModel<FakePerson>()); 
    } 
} 

Wenn ich zu erreichen schaffen sie meine Ansicht, wie alles funktioniert wie folgt erwartet

<%@ Page Language="C#" 
    MasterPageFile="~/Views/Shared/Site.Master" 
    Inherits="System.Web.Mvc.ViewPage<HomeViewModel<FakePerson>>" %> 
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> 
    <%: Html.DisplayFor(m=>m.Person.FirstName) %> 
    <%: Html.DisplayFor(m=>m.Person.LastName) %> 
</asp:Content> 

Allerdings möchte ich nicht direkt auf FakePerson in der Ansicht für den Fall ab, die ich einige andere IPerson Implementierung übergeben wollen, so dass ich versucht, die Seite Richtlinie

<%@ Page Language="C#" 
    MasterPageFile="~/Views/Shared/Site.Master" 
    Inherits="System.Web.Mvc.ViewPage<HomeViewModel<IPerson>>" %> 

Aber natürlich zu ändern, die, nach einem ganzen Tag von Ausmisten etwa, ich habe mehr graue Haare und keine Ahnung, was als nächstes tun nicht funktionieren, so.

Kann jemand bitte helfen.

[UPDATE]

Einige beraten hat sugested, dass ich eine kovariante Schnittstelle verwendet werden soll; Definieren Sie eine nicht-generische Schnittstelle und verwenden Sie sie in der Ansicht. Leider habe ich das versucht, aber es gibt eine zusätzliche Implikation. Ich möchte die Htmlhelper Funktion können alle Daten Annotations Attribute zugreifen, die in der IPerson abgeleiteten Klasse definiert werden können

public class FakePerson : IPerson 
{ 
    public FakePerson() 
    { 
     FirstName = "Foo"; 
     LastName = "Bar"; 
    } 

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

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

Während also eine kovariante Schnittstelle auf diese Weise funktioniert, teilweise, den abgeleiteten Typ für den Zugriff durch das ViewModel; Da die Ansicht in die Schnittstelle eingegeben wurde, scheinen die Attribute nicht zugänglich zu sein.

Gibt es in der Ansicht vielleicht einen Weg, Zugang zu diesen Attributen zu bekommen, vielleicht mit Reflektion. Oder könnte es einen anderen Weg geben, um die Ansicht zum Generischen zu schreiben.

+0

Wie kann man erwarten, dass Ihre Sicht auf Zugang abgeleitet Daten, aber Sie wollen es nicht von den abgeleiteten Daten abhängig machen? Anders ausgedrückt: Wenn Sie auf Daten zugreifen, die nur für FakePerson gelten, hängt Ihre Ansicht von FakePerson ab. Daher sollten Sie Ihr Modell zu einem FakePerson machen. Wenn Sie ein anderes Objekt übergeben, wird es beim Zugriff auf den FakePerson Fehler geben. –

+0

Sie können Datenannotationen auf keinen Fall mit Schnittstellen verwenden, da Attribute nicht vererbt werden (siehe http://stackoverflow.com/questions/540749/can-ac-sharp-class-inherit-attributes-from-its-interface) –

Antwort

0

Ihr Code ist fast korrekt; Sie müssen nur eine HomeViewModel<IPerson> (nicht FakePerson) in der Steuerung übergeben.

Sie könnten auch eine kovariante Schnittstelle für das Modell in der Ansicht verwenden, aber das ist wahrscheinlich übertrieben.

+0

Danke für die schnelle Antwort. Meinst du den Code – ricardo

+0

öffentliche Klasse HomeController: Controller { öffentliche ViewResult Index() { Rücksendeansicht (neue HomeViewModel ()); } } – ricardo

+0

public ViewResult Index() {return View (neues HomeViewModel ()) ;. Dies führt zu einem Laufzeitfehler, da IPerson kein konkreter Typ ist und der Punkt darin besteht, eine abgeleitete IPerson-Klasse an die Ansicht übergeben zu können. – ricardo

0

Lassen Sie Ihre ViewModel-Klasse eine nicht generische (oder generische kovariante) Schnittstelle implementieren und ändern Sie dann die Ansicht, um diese Schnittstelle anstelle der konkreten Klasse zu verwenden.

+0

Danke. Ich habe darüber nachgedacht und es einfach wieder ausprobiert, aber diese Approach gibt mir nicht alle Eigenschaften, auf die ich gehofft hatte.Bitte beachten Sie das Update zu meiner Frage. – ricardo

2

Ich habe erfolgreich Covariant zu arbeiten, das heißt, die Sicht auf eine abstrakte Basisklasse binden. In der Tat, was ich habe, ist eine Bindung an eine Liste <MyBaseClass>. Dann erstelle ich eine spezifische View, die für jede Unterklasse stark typisiert ist. Das sorgt für die Bindung.

Aber Rebinding wird fehlschlagen, weil der DefaultModelBinder nur über abstrakte Basisklasse bekannt ist und Sie eine Ausnahme wie "kann abstrakte Klasse nicht erstellen" erhalten. Die Lösung ist eine Eigenschaft auf Ihrer Basisklasse wie diese hat:

public virtual string BindingType 
    { 
     get 
     { 
      return this.GetType().AssemblyQualifiedName; 
     } 
    } 

binden, die zu einem versteckten Eingang Ihrer Ansicht. Dann ersetzen Sie Ihren standardmäßigen ModelBinder durch einen benutzerdefinierten in Global.asax:

Und in Ihrem benutzerdefinierten Modell Binder abfangen Sie die Bindung. Wenn es für einen Ihres bekannten abstrakten Typen ist, können Sie die Binding Eigenschaft analysieren und den Modelltyp ersetzen, so dass Sie eine Instanz der Unterklasse erhalten:

public class CustomModelBinder : DefaultModelBinder 
{ 
    private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 

    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType) 
    { 
     if (modelType.IsInterface || modelType.IsAbstract) 
     { 
      // This is our convention for specifying the actual type of a base type or interface. 
      string key = string.Format("{0}.{1}", bindingContext.ModelName, Constants.UIKeys.BindingTypeProperty);    
      var boundValue = bindingContext.ValueProvider.GetValue(key); 

      if (boundValue != null && boundValue.RawValue != null) 
      { 
       string newTypeName = ((string[])boundValue.RawValue)[0].ToString(); 
       logger.DebugFormat("Found type override {0} for Abstract/Interface type {1}.", modelType.Name, newTypeName); 

       try 
       { 
        modelType = System.Type.GetType(newTypeName); 
       } 
       catch (Exception ex) 
       { 
        logger.ErrorFormat("Error trying to create new binding type {0} to replace original type {1}. Error: {2}", newTypeName, modelType.Name, ex.ToString()); 
        throw; 
       } 
      } 
     } 

     return base.CreateModel(controllerContext, bindingContext, modelType); 
    } 

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) 
    { 
     if (propertyDescriptor.ComponentType == typeof(BaseParameter)) 
     { 
      string match = ".StringValue"; 
      if (bindingContext.ModelName.EndsWith(match)) 
      { 
       logger.DebugFormat("Try override for BaseParameter StringValue - looking for real type's Value instead."); 
       string pattern = match.Replace(".", @"\."); 
       string key = Regex.Replace(bindingContext.ModelName, pattern, ".Value"); 
       var boundValue = bindingContext.ValueProvider.GetValue(key); 
       if (boundValue != null && boundValue.RawValue != null) 
       { 
       // Do some work here to replace the base value with a subclass value... 
        return value; 
       } 
      } 
     } 

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

Hier meine abstrakte Klasse ist BaseParameter und ich ersetzt der String Eigenschaft mit einem anderen Wert als die Unterklasse (nicht gezeigt).

Beachten Sie, dass die mit der Unterklasse verknüpften Formularwerte nicht automatisch abgerundet werden können, obwohl der Modellbinder nur die Eigenschaften der Basisklasse sehen kann. In meinem Fall musste ich nur einen Wert in GetValue ersetzen und stattdessen aus der Unterklasse holen, also war es einfach. Wenn Sie viele Unterklassen-Eigenschaften binden müssen, müssen Sie etwas mehr Arbeit erledigen und sie aus dem Formular holen (ValueProvider [0]) und die Instanz selbst füllen.

Beachten Sie, dass Sie einen neuen Modellbinder für einen bestimmten Typ hinzufügen können, damit Sie die generische Typüberprüfung vermeiden können.

0

Ich habe eine Schnittstelle geschaffen

public interface ITestModel 
    { 
     string FirstName { get; set; } 
     string LastName { get; set; } 
     string Address { get; set; } 
     int Age { get; set; } 
    } 

Klasse erstellen und von dieser Schnittstelle geerbt

class TestModel : ITestModel 
    { 
     public string FirstName { get; set; } 
     public string LastName { get; set; } 
     public string Address { get; set; } 
     public int Age { get; set; } 

    } 

erstellt Instanz der Klasse in Controller

public ActionResult TestMethod() 
     { 
      ITestModel testModel; 
      testModel = new TestModel(); 
      testModel.FirstName = "joginder"; 
      testModel.Address = "Lovely Home"; 
      testModel.LastName = "singh"; 
      testModel.Age = 36; 
      return View("testMethod", testModel); 
     } 

anzeigen wie auf diese Weise erstellt

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<TestProject.ITestModel>" %> 

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> 
    TestMethod 
</asp:Content> 

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> 

    <h2>TestMethod</h2> 

    <fieldset> 
     <legend>Fields</legend> 

     <div class="display-label">FirstName</div> 
     <div class="display-field"><%: Model.FirstName %></div> 

     <div class="display-label">LastName</div> 
     <div class="display-field"><%: Model.LastName %></div> 

     <div class="display-label">Address</div> 
     <div class="display-field"><%: Model.Address %></div> 

     <div class="display-label">Age</div> 
     <div class="display-field"><%: Model.Age %></div> 

    </fieldset> 
    <p> 
     <%: Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> | 
     <%: Html.ActionLink("Back to List", "Index") %> 
    </p> 

</asp:Content> 

Jetzt habe ich ein beliebiges Objekt von einer ähnlichen Schnittstelle passieren kann

Ich hoffe, es wird Ihnen helfen :)