Der Sinn des ViewModels – oder: was sagt die Fehlermeldung “No parameterless constructor defined for this object” aus?

Das MVC Pattern spricht von Model – View – Controller. Jedoch kann es auch im ASP.NET MVC sinnvoll sein, ein ViewModel zu verwenden. Gute Architektur ist durch nichts zu ersetzen und beim falschen oder “abgekürzten” Einsatz eines Frameworks wie zum Beispiel ASP.NET MVC stellt man bald fest: man hält sich besser an Architektur-Empfehlungen als das Rad neu zu erfinden.

Konkret wollte ich ein Eingabeformular gestalten, in dem eine DropDownliste verwendet wird. Im System.Web.Mvc Namespace gibt es eine Klasse “SelectList” und diese ist für die Darstellung einer Auswahlliste sehr gut geeignet, da sie auch direkt im @Html.DropDownListFor unterstützt wird.

 

Der einfache und fehlerhafte Weg…

Möchte man sich die Arbeit erleichtern, kommt man schnell auf die Idee ein Model anzulegen und in diesem auch die Select-Liste zu implementieren. Ich habe dies in der Klasse “DatenEingabeModel” so getan und im Konstruktor wird die Liste mit den möglichen Auswahlwerten gefüllt. Zusätzlich enthält die Klasse das Property “AusgewaehlterWert”, um die Auswahl des Benutzers abzuspeichern.

   1:      public class DatenEingabeModel
   2:      {
   3:          public string AusgewaehlterWert { get; set; }
   4:   
   5:          private SelectList werteListe = null;
   6:          public SelectList WerteListe
   7:          {
   8:              get { return werteListe; }
   9:          }
  10:   
  11:          public DatenEingabeModel()
  12:          {
  13:              var tempListe = new List<SelectListItem>();
  14:              tempListe.Add(new SelectListItem() { Text = "Wert A", Value = "A" });
  15:              tempListe.Add(new SelectListItem() { Text = "Wert B", Value = "B" });
  16:              tempListe.Add(new SelectListItem() { Text = "Wert C", Value = "C" });
  17:   
  18:              werteListe = new SelectList(tempListe, "Value", "Text");
  19:          }
  20:      }

Das Model wird im DatenEingabeController verwendet. Es gibt eine Action “Save”, diese wird vom View durch das Formular aufgerufen. Sobald die Daten gespeichert wurden, kommt es zu einem Redirect zu einer anderen Action, “OK”, die einen View liefert, der die eingegeben Daten nochmals darstellt und dem Benutzer mitteilt, dass das Speichern erfolgreich war. Um die Daten anzeigen zu können, wird das Model an den View in der OK-Action weitergereicht.

   1:      public class DatenEingabeController : Controller
   2:      {
   3:          // GET: DatenEingabe
   4:          public ActionResult Index()
   5:          {
   6:              DatenEingabeModel model = new DatenEingabeModel();
   7:              return View(model);
   8:          }
   9:   
  10:          [HttpPost, ActionName("Save")]
  11:          public ActionResult Save(DatenEingabeModel model)
  12:          {
  13:              // Code zum Speichern der Daten
  14:              return RedirectToAction("OK", model);
  15:          }
  16:   
  17:          public ActionResult OK(DatenEingabeModel model)
  18:          {
  19:              return View(model);
  20:          }
  21:      }

Zuletzt noch der Index View: dieser verwendet @Html.DropDownListFor um das Property “AusgewaehlterWert” als DropDownListe anzuzeigen. (Zeile 18)

   1:  @model ViewModelDemo.Models.DatenEingabeModel
   2:  @{
   3:      ViewBag.Title = "Index";
   4:  }
   5:  <h2>Index</h2>
   6:   
   7:  @using (Html.BeginForm("Save", "DatenEingabe"))
   8:  {
   9:      @Html.AntiForgeryToken()
  10:      
  11:      <div class="form-horizontal">
  12:          <h4>DatenEingabeModel</h4>
  13:          <hr />
  14:          @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  15:          <div class="form-group">
  16:              @Html.LabelFor(model => model.AusgewaehlterWert, 
htmlAttributes: new { @class = "control-label col-md-2" })
  17:              <div class="col-md-10">
  18:                  @Html.DropDownListFor(model => model.AusgewaehlterWert, Model.WerteListe, 
 new { htmlAttributes = new { @class = "form-control" } })

19: @Html.ValidationMessageFor(model => model.AusgewaehlterWert, "",

new { @class = "text-danger" })

  20:              </div>
  21:          </div>
  22:   
  23:          <div class="form-group">
  24:              <div class="col-md-offset-2 col-md-10">
  25:                  <input type="submit" value="Save" class="btn btn-default" />
  26:              </div>
  27:          </div>
  28:      </div>
  29:  }
  30:   
  31:  <div>
  32:      @Html.ActionLink("Back to List", "Index")
  33:  </div>
  34:   
  35:  @section Scripts {
  36:      @Scripts.Render("~/bundles/jqueryval")
  37:  }

So weit, so gut. Eigentlich würde man hierbei keine Probleme erwarten. Doch leider kommt es anders. Sobald der Benutzer den Save Button drückt, wird noch die Save Action durchgeführt, und beim Redirect zur OK-Action passiert folgendes:

image

Die Fehlermeldung “No parameterless constructor defined for this object” deutet darauf hin, dass wir einen Konstruktor vergessen haben. Die parameterlosen Konstruktoren werden meist beim Deserialisieren benötigt. So ist es auch hier. Nur leider haben wir das nicht unter Kontrolle. Das Problem ist die Klasse “SelectList”, die keinen parameterlosen Konstruktor hat.

Dieser einfache Weg führt also bald zu einem Problem.

Was ist falsch?

Es gibt extra eine Klasse “SelectList” und diese kann man nicht im Model verwenden? Das Problem ist, das Model enthält nur die Daten, die der Benutzer eingibt. Jene Daten, die für die Anzeige eines Views notwendig sind, dürfen nicht im Model sein. Wenn man es sauber trennen möchte, gibt es hierfür das ViewModel. Das ist eine neue Modelklasse die neben den Userdaten (Model) auch die Daten für die Oberfläche beinhalten kann. In unserem Fall sind das die Werte, die in der Werte-Liste angezeigt werden sollen.

Korrekt ist es also ein ViewModel anzulegen, welches das Model beinhaltet und zusätzlich die Werteliste. Hier die korrigierte Model Klasse und dazu passend ein ViewModel:

   1:      public class DatenEingabeModel
   2:      {
   3:          public string AusgewaehlterWert { get; set; }
   4:      }
   5:   
   6:   
   7:      public class DatenEingabeViewModel
   8:      {
   9:          private DatenEingabeModel theModel = null;
  10:          public DatenEingabeModel TheModel
  11:          {
  12:              get { return theModel; }
  13:          }
  14:   
  15:          private SelectList werteListe = null;
  16:          public SelectList WerteListe
  17:          {
  18:              get { return werteListe; }
  19:          }
  20:          public DatenEingabeViewModel(DatenEingabeModel model)
  21:          {
  22:              theModel = model;
  23:              var tempListe = new List<SelectListItem>();
  24:              tempListe.Add(new SelectListItem() { Text = "Wert A", Value = "A" });
  25:              tempListe.Add(new SelectListItem() { Text = "Wert B", Value = "B" });
  26:              tempListe.Add(new SelectListItem() { Text = "Wert C", Value = "C" });
  27:   
  28:              werteListe = new SelectList(tempListe, "Value", "Text");
  29:          }
  30:      }

Im Konstruktor des ViewModels wird das eigentliche Datenmodel übergeben. Die Werteliste ist nun im ViewModel und das Model selbst enthält nur mehr Daten, die der Benutzer erfasst und die später in eine Datenbank geschrieben werden.

Der Controller übergibt in der Index und der OK Action ein “DatenEingabeViewModel”. Nur im Save wird lediglich das Model selbst übergeben und im Redirect auch an die OK Action übergeben.

   1:      public class DatenEingabeController : Controller
   2:      {
   3:          // GET: DatenEingabe
   4:          public ActionResult Index()
   5:          {
   6:              DatenEingabeModel model = new DatenEingabeModel();
   7:              return View(new DatenEingabeViewModel(model));
   8:          }
   9:   
  10:          [HttpPost, ActionName("Save")]
  11:          public ActionResult Save(DatenEingabeModel model)
  12:          {
  13:              // Code zum Speichern der Daten
  14:              return RedirectToAction("OK", model);
  15:          }
  16:   
  17:          public ActionResult OK(DatenEingabeModel model)
  18:          {
  19:              return View(new DatenEingabeViewModel(model));
  20:          }
  21:      }

Der View muss noch angepasst werden. Als Model Klasse wird nun DatenEingabeViewModel verwendet. Daher müssen die angezeigten Properties noch angepasst werden. In meinem Fall verwende ich nun die Variable vm, denn diese zeigt mir besser an, dass ich nun mit dem ViewModel arbeite.

   1:  @model ViewModelDemo.Models.DatenEingabeViewModel
   2:  @{
   3:      ViewBag.Title = "Index";
   4:  }
   5:  <h2>Index</h2>
   6:   
   7:  @using (Html.BeginForm("Save", "DatenEingabe"))
   8:  {
   9:      @Html.AntiForgeryToken()
  10:      
  11:      <div class="form-horizontal">
  12:          <h4>DatenEingabeModel</h4>
  13:          <hr />
  14:          @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  15:          <div class="form-group">
  16:              @Html.LabelFor(vm => vm.TheModel.AusgewaehlterWert, 
htmlAttributes: new { @class = "control-label col-md-2" })
  17:              <div class="col-md-10">
  18:                  @Html.DropDownListFor(vm => vm.TheModel.AusgewaehlterWert, Model.WerteListe, 
new { htmlAttributes = new { @class = "form-control" } })
  19:                  @Html.ValidationMessageFor(vm => vm.TheModel.AusgewaehlterWert, "", new { @class = "text-danger" })
  20:              </div>
  21:          </div>
  22:   
  23:          <div class="form-group">
  24:              <div class="col-md-offset-2 col-md-10">
  25:                  <input type="submit" value="Save" class="btn btn-default" />
  26:              </div>
  27:          </div>
  28:      </div>
  29:  }
  30:   
  31:  <div>
  32:      @Html.ActionLink("Back to List", "Index")
  33:  </div>
  34:   o
  35:  @section Scripts {
  36:      @Scripts.Render("~/bundles/jqueryval")
  37:  }

Fazit

ASP.NET MVC ist einfach zu verwenden, allerdings gilt es sich an die Regeln zu halten. In ein Datenmodell gehören nur die Daten, die gespeichert werden. Daten, die für die Oberfläche notwendig sind, müssen in ein ViewModel ausgelagert werden. Ein gutes Indiz, wann ein ViewModel verwendet werden soll, ist, wenn im View etwas angezeigt werden soll, das nicht in eine Datenbank geschrieben wird.

Kommentare sind geschlossen