Archiv der Kategorie: MVC3

ASP.NET MVC HandleUnknownAction zum Zurückgeben des aufgerufenen Views verwenden


In vielen meiner MVC Projekten, in denen ich auf AngularJS für das UI setze, verwende ich immer noch die Standard MVC Views mit “Layout = null”, um das HTML Template auch .NET Seitig noch manipulieren zu können. Dabei gibt es dann eine Menge Controller Funktionen die einfach nur ein ActionResult mit dem zugehörigen View zurückgeben z.B.:

public class TodoController : Controller
{
   #region Views
   public ActionResult TodoOverview()
   {
       return View();
   }
   #endregion

   #region Modals
   public ActionResult TodoEditModal()
   {
       return View();
   }
   #endregion
}

Dabei handelt es sich immer wieder um den gleichen Boilerplate Code und zum Glück gibt es auch hier eine Möglichkeit dies mit ASP.NET MVC zu optimieren und nur noch eine Funktion “HandleUnknownAction” zu verwenden.

public class TodoController : Controller
{
    protected override void HandleUnknownAction(string actionName)
    {
       try
        {
           this.View(actionName).ExecuteResult(this.ControllerContext);
         }
         catch (System.Exception)
         {
            this.View("404").ExecuteResult(this.ControllerContext);
          }
     }
}

Diese Überschriebene Funktion wird immer aufgerufen, wenn zum zugehörigen Controller Aufruf keine passende Action im aktuellen Controller gefunden werden kann und versucht dann automatisch den übergebenen Actionnamen auf einen existierenden View zu Mappen.

Wenn es hier noch andere oder bessere Lösungen gibt freue ich mich über einen Kommentar, aber aktuell habe ich nichts besseres finden können.

Quelle:

http://stephenwalther.com/archive/2008/07/21/asp-net-mvc-tip-22-return-a-view-without-creating-a-controller-action

Advertisements

AngularJS ng-pattern mit CamelCase Pattern als Beispiel


AngularJS bietet die unterschiedlichsten Direktiven zum Validieren von Eingabefeldern an und darunter befindet sich auch die Direktive “ng-pattern” mit der Eingabewerte über Reguläre Ausdrücke validiert werden können. Da ich mit Regulären Ausdrücken nicht so viel zu tun habe, habe ich mich bei der ersten Verwendung etwas schwer getan und leider gab es auch kein direktes Beispiel in der Dokumentation.

Ich habe mir einen Regulären Ausdruck erstellt der prüfen soll ob die Eingabe in “CamelCase” Schreibweise eingegeben wurde.

Regulärer Ausdruck:  /^((^[A-Z][a-z0-9]+)([A-Z][a-z0-9]+)*)$/

Wenn hier jemand eine effektiveren Ausdruck kennt dann freue ich mich sehr über einen passenden Kommentar.

In AngularJS kann der Reguläre Ausdruck direkt dem “ng-pattern” übergeben werden

<input type="text" 
             name="ccName" 
             data-ng-model="ViewModel.CamelCaseName" 
              data-ng-pattern="/^((^[A-Z][a-z0-9]+)([A-Z][a-z0-9]+)*)$/" 
              placeholder="Enter CamelCase String">

oder man speichert ihn im Controller in einer Variablen und übergibt diese dem “ng-pattern”. Achtung um das Regex Pattern keine Anführungszeichen setzen!

...
  $scope.ViewModel = {
        CamelCaseName: "TestCamleCase",
        CamelCasePattern: /^((^[A-Z][a-z0-9]+)([A-Z][a-z0-9]+)*)$/
    };
...

im “ng-pattern” die Variable setzen statt des Patterns.

<input type="text" 
             name="ccName" 
             data-ng-model="ViewModel.CamelCaseName" 
              data-ng-pattern="ViewModel.CamelCasePattern"
              placeholder="Enter CamelCase String">

Zum Erstellen und überprüfen von Regulären Ausdrücken verwende ich auch gern die folgende Webseite:

https://regex101.com/r/gM3qM0/1#javascript

Das ganze Codebeispiel findet man auch wieder in Codeplex.

Leichtgewichtige Controller – wohin mit meiner Controllerlogik


Auch wenn man bisher nur an kleineren ASP.NET MVC Projekten gearbeitet hat, kann der Code in einem Controller sehr schnell, sehr unübersichtlich werden. Denn im Controller wird meist zu viel Logik “abgelegt”, die sich eigentlich sehr gut auslagern lässt.

In meinen Webprojekten versuche ich, dass ein Controller nur prüft ob der Modelstate Valide ist und um dann den passenden View aufzurufen/laden (“routen”) . Das Laden der Viewmodeldaten übernimmt bei mir dann entweder Variante 1, “das intelligente Model” oder Variante 2 der “Modelbuilder”, natürlich immer nur eine Variante pro Projekt. Damit erhält man einen sehr übersichtlichen und leichtgewichtigen Controller.

Variante 1: Das intelligente Model

Hier enthält das jeweilige Viewmodel, nicht nur die Properties die angezeigt werden sollen für den jeweiligen View, sondern es enthält ebenfalls die Logik sich selbst mit Daten zu füllen oder sich selbst zu speichern. Bei dieser Variante erntet man oft Kritik, da viele die starre Meinung vertreten, das ein Model niemals Logik enthalten sollte. Dem stimme ich prinzipiell auch zu, aber ich finde hier stellt es eine gute Alternative dar, auch dem Model mal etwas Logik zu verpassen.

In meinem Fall benutzte ich Dependency Injection (Ninject) um im Controller das passende Repository einzubinden. Das Repository übergebe ich dem Model in der jeweiligen Funktion, wo das Repository benötigt wird. Wichtig ist das man darauf achtet, wenn es sich um ein Postbackmodel handelt, das ein Parameterloser Konstruktor existiert, sonst kann der Modelbinder von .NET keine Instanz erstellen und das Model mit Daten füllen.

public class AutoController : Controller
{
    #region Injections
    [Inject]
    public IAutoRespository AutoRespository { protected get; set; }
    #endregion

    #region View Functions
    public ActionResult AutoList()
    {
        AutoListModel model = new AutoListModel();
        model.Load(AutoRespository);
        return View("AutoList", "Auto", model);
    }

    public ActionResult AutoView(int id)
    {
        AutoViewModel model = new AutoViewModel();
        model.Load(id, AutoRespository);
        return View("AutoView", "Auto", model);
    }

    public ActionResult AutoEdit(int id)
    {
        AutoEditModel model = new AutoEditModel();
        model.Load(id, AutoRespository);
        return View("AutoEdit", "Auto", model);
    }

    [HttpPost]
    public ActionResult AutoSave(AutoEditModel model)
    {
        if (ModelState.IsValid)
        {
            model.SaveOrUpdate(AutoRespository);
            RedirectToAction("AutoView", "Auto", new {id = model.Id});
        }

        return View();
    }
    #endregion
}

Dann z.B. noch das passende AutoEditModel welches eine “Load” und eine “SaveOrUpdate” Methode enthält um sich selbst mit Daten zu füllen oder sich selbst zu speichern.

public class AutoEditModel : IAutoEditModel
{
    #region Member
    public int Id { get; set; }

    [Required]
    public string Marke { get; set; }

    [Required]
    public string Nummernschild { get; set; }
    #endregion

    #region Public Functions
    public void Load()
    {
        Marke = string.Empty;
        Nummernschild = string.Empty;
        Id = 0;
    }

    /// <summary>
    /// Füllen unseres Models mit den passenden Daten.
    /// </summary>
    /// Autorepository für die Abfragen
    public void Load(int autoId, IAutoRespository autoRespository)
    {
        try
        {
            var auto = autoRespository.Load(autoId);
            if (auto == null)
            {
                //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
                return;
            }

            Id = autoId;
            Marke = auto.Marke;
            Nummernschild = auto.Nummernschild;
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
    }

    /// <summary>
    /// Speichern oder Aktualisieren der aktuellen Modeldaten
    /// </summary>
    public void SaveOrUpdate(IAutoRespository autoRespository)
    {
        try
        {
            Auto auto = new Auto();
            auto.Id = Id;
            auto.Marke = Marke;
            auto.Nummernschild = Nummernschild;

            var savedAuto = autoRespository.AddOrUpdateAuto(auto);
            Id = savedAuto.Id;
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
    }
    #endregion
}

Es ist auch möglich die Models noch weiter aufzuteilen, das man ein Model zum Füllen/Anzeigen des “Editmodels” hat und ein “Postbackmodel” zum Speichern der Daten. Im Beispiel handelt es sich um ein Model welches als Anzeige- und Postbackmodel benutzt wird. Die Viewmodels können auch sehr gut mit Unit Tests getestet werden, da im Normalfall keinen Verweis auf den HTTP Context enthalten ist und alle notwendigen Abhängigkeiten wie das Repository bei Bedarf entsprechend übergeben werden.

Wenn man wie bereits erwähnt, die Models sehr strikt in Anzeige- und Postbackmodels trennt, dann ist es auch kein Problem im Controller zumindest die Anzeigemodels direkt mit zu Injecten.

In der Solution selbst sieht die Ordner- und Dateistruktur dann z.B. folgendermaßen aus.

image

Variante 2: Modelbuilder

In Variante 2 wird wieder alles strikt getrennt und man hat ein sauberes Model und einen sauberen Controller. Hier wird ein Modelbuilder erstellt der die Daten für unsere entsprechenden Models lädt oder speichert. Hier muss man selbst prüfen ob man für jedes Viewmodel einen eigenen Modelbuilder erstellt oder wie in meinem Beispiel, nur einen Modelbuilder.

Es wird der Modelbuilder in den Controller injected und das Repository wird hier direkt in den Modelbuilder injected.

public class PersonController : Controller
{
    #region Initialize
    public PersonController(IPersonModelBuilder personModelBuilder)
    {
        if (personModelBuilder == null)
        {
            throw new NullReferenceException("PersonModelBuilder ist Null.");
        }

        _personModelBuilder = personModelBuilder;
    }

    private IPersonModelBuilder _personModelBuilder;
    #endregion

    #region Views
    /// <summary>
    /// Model mit einem leeren PersonenModel anlegen
    /// </summary>
    public ActionResult AddPerson()
    {   
       return View(_personModelBuilder.Load());
    }
    
    /// <summary>
    /// Erstellen einer neuen Person
    /// </summary>
    [HttpPost]
    public ActionResult AddPerson(PersonEditModel editModel)
    {
        if (ModelState.IsValid)
        {
            _personModelBuilder.AddOrSavePerson(editModel);
            return RedirectToAction("PersonList");
        }

        return View(editModel);
    }

    /// <summary>
    /// Bearbeiten einer einzelnen Person mit der übergebenen Id
    /// </summary>
    public ActionResult EditPerson(int id)
    {
        return View(_personModelBuilder.Load(id));
    }

    /// <summary>
    /// Speichern der Personendaten die übergeben wurden im Post
    /// </summary>
    [HttpPost]
    public ActionResult EditPerson(PersonEditModel editModel)
    {
        if (ModelState.IsValid)
        {
            _personModelBuilder.AddOrSavePerson(editModel);
            return RedirectToAction("Person", "Person", new {id = editModel.Id});
        }

        return View(editModel);
    }

    /// <summary>
    /// Anzeigen der Details einer einzelnen Person
    /// </summary>
    public ActionResult Person(int id)
    {
        return View(_personModelBuilder.Load(id));
    }

    /// <summary>
    /// Laden aller Personen 
    /// </summary>
    public ActionResult PersonList()
    {
        return View();
    }
    #endregion 
}

Der Modelbuilder Selbst enthält in meinem Fall alle wichtigen Funktionen zum Füllen der entsprechenden Personen Models. Hier werden die Daten vom Repository auf das passende Viewmodel gemappt bzw. die entsprechenden Daten gespeichert.

public class PersonModelBuilder : IPersonModelBuilder
{
    #region Initialize
    /// <summary>
    /// Initialize und der ModelContainer wird per Di übergeben.
    /// </summary>
    public PersonModelBuilder(IPersonRespository personRespository)
    {
        if (personRespository == null)
        {
            throw new NullReferenceException("PersonRespository ist Null.");
        }
        _personRespository = personRespository;
    }

    private IPersonRespository _personRespository;
    #endregion

    #region Public Functions
    /// <summary>
    /// Laden aller Personendaten
    /// </summary>
    public PersonListModel LoadAllPersons()
    {
        PersonListModel model = new PersonListModel();
        try
        {
            //abrufen der Personendaten aus der Datenbank
            var persons = _personRespository.AllPersons();

            //Füllen unseres ListModels mit den passenden Personendaten
            foreach (EfModel.Person person in persons)
            {
                PersonViewModel viewModelPerson = new PersonViewModel();
                MapPersonViewModel(viewModelPerson, person);
                model.PersonList.Add(viewModelPerson);
            }

            model.AnzahlAllerAutos = 10;
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
        return model;
    }

    /// <summary>
    /// Laden eines leeren ViewModels
    /// </summary>
    public PersonViewModel Load()
    {
        PersonViewModel model = new PersonViewModel();
        try
        {
            model.Alter = 18;
            model.Nachname = string.Empty;
            model.Vorname = string.Empty;
            model.Id = 0;

            //Da Neues Objekt, kann es auch keine Autos haben :-)
            model.AnzahlAutos = 0;
            model.IsAltePerson = model.Alter &gt; 30;
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
        return model;
    }

    /// <summary>
    /// Laden einer Person mit der übergebenen PersonenId
    /// </summary>
    /// Id der Person die geladen werden soll
    public PersonViewModel Load(int personId)
    {
        PersonViewModel model = new PersonViewModel();
        try
        {
            var dbPerson = _personRespository.Load(personId);
            if (dbPerson == null)
            {
                //TODO Fehlermeldung das die Person nicht gefunden werden konnte.
                return model;
            }

            MapPersonViewModel(model, dbPerson);
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
        return model;
    }

    /// <summary>
    /// Erstellen oder Updaten des übergebenen PersonenEditModels
    /// </summary>
    public PersonViewModel AddOrSavePerson(PersonEditModel editModel)
    {
        PersonViewModel viewModel = new PersonViewModel();
        try
        {
            EfModel.Person person = new EfModel.Person();
            person.Id = editModel.Id;
            person.Nachname = editModel.Nachname;
            person.Vorname = editModel.Vorname;
            person.Benutzername = editModel.Benutzername;
            person.Alter = editModel.Alter;

            var dbPers = _personRespository.AddOrUpdatePerson(person);
            MapPersonViewModel(viewModel, dbPers);
        }
        catch (Exception exception)
        {
            //TODO Fehlermeldungen kapseln, damit diese entsprechend im View angezeigt werden kann.
        }
        return viewModel;
    }
    #endregion

    #region private Functions
    /// <summary>
    /// Mappen unserer DB Person auf das aktuelle ViewModel
    /// </summary>
    /// unser aktuelles ViewModel
    /// DB Personen Objekt
    private void MapPersonViewModel(PersonViewModel viewModel, EfModel.Person person)
    {
        viewModel.Alter = person.Alter;
        viewModel.Nachname = person.Nachname;
        viewModel.Vorname = person.Vorname;
        viewModel.Benutzername = person.Benutzername;
        viewModel.Id = person.Id;
        viewModel.AnzahlAutos = 10;
        viewModel.IsAltePerson = person.Alter &gt; 30;
    }
    #endregion
}

Das Model ist in der Modelbuilder Varianten wieder nur ein simples Viewmodel, nur mit Properties.

public class PersonEditModel
{
    #region Member
    public int Id { get; set; }

    [Required]
    public string Vorname { get; set; }

    [Required]
    public string Nachname { get; set; }

    [Required]
    public string Benutzername { get; set; }

    public int Alter { get; set; }
    #endregion
}

Die Ordner- und Dateistruktur in der Solution sieht in der Modelbuildervariante z.B. folgendermaßen aus.

image

Zusammenfassung

Beide Varianten lösen natürlich wieder nur einen kleinen Teil der “Probleme” auf die man beim Erstellen von Viewmodels mit der Zeit stößt und sicherlich müssen beide Varianten auch noch an die jeweilige Implementierung angepasst werden. Wenn man sich aber erst einmal daran gewöhnt hat, das ein Controller auch wirklich sauber und übersichtlich aussehen kann, dann benutzt man seine Bevorzugte saubere Implementierung in all seinen Webprojekten.

Ich habe bisher mit beiden Varianten gearbeitet und ziehe aktuell die mit dem Modelbuilder vor, da man hier den Modelbuilder in den Controller injected und man den Controller sehr leicht mit Unit Tests testen kann.

Da es sich hier nur um ein Beispiel handelt wie beide Varianten prinzipiell umgesetzt werden, handelt es sich um sehr “einfache” Beispiele die im Detail natürlich noch besser umgesetzt werden können.

Ein einfaches Beispiel mit den Passenden Models und Controllern findet man in meinem Codeplex Account. Wenn man bei Google mehr über das Thema erfahren möchte, sucht man am besten nach “Thin / Skinny Controller .NET MVC” hier gibt auch einige Beispiele mit Pros und Cons zu den einzelnen Varianten.

Sollte ich noch weitere Möglichkeiten für einen übersichtlicheren Controller übersehen haben, dann freue ich mich immer gerne über entsprechende Hinweise in den Kommentaren.

ASP.NET MVC und Dependency Injection für Views – Part 3


Einen View mit Dependency Injection (Di) zu initialisieren ist möglich, sollte aber nicht prinzipiell verwendet werden, sondern nur wenn z.B. ein Lokalisierungsservice direkt Strings auf dem View ausgeben und übersetzten soll. Denn im View soll normalerweise keine Logik untergebracht werden, sondern nur zur reinen Anzeige der Modeldaten dienen.

Um einen View mit Hilfe von Di zu initialisieren sind die folgenden Schritte notwendig.

Überschreiben der Standardklasse für die ViewCreation “WebViewPage”, dafür müssen wir unsere eigene Klasse anlegen die von “WebViewPage” ableitet.

 public abstract class CustomDiWebViewPage<TModel> : WebViewPage<TModel>
    {
        /// <summary>
        /// Lokalisation Interface, welches per Property Injektion vom DiFramework initialisiert wird.
        /// </summary>
        public ILocalizeable Localizeable { get; set; }
    }

Achtung wichtig ist hier das “abstract”. Das Interface und die passende Implementierung stellen wir zur Verfügung und könnte z.B. folgendermaßen aussehen:

 public interface ILocalizeable
 {
        /// <summary>
        /// den passend Lokalisierten String zurückgeben
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        string GetLocalizedString(string name);
 }

 /// <summary>
 /// Interface und Implementierung nur zu Demonstrationszwecken im gleichen Projekt!
 /// </summary>
 public class Localizeable : ILocalizeable
 {
     /// <summary>
     /// den passend Lokalisierten String zurückgeben
     /// </summary>
    public string GetLocalizedString(string name)
     {
         //TODO Lokalisierung entsprechend einbinden.
        return name + " localized";
     }
 }

Damit MVC auch weiß welche Klasse für das “anzeigen” des Views zuständig ist, gibt es hier zwei Möglichkeiten:

1. In der web.config, die sich im Views Verzeichnis befindet die Zeile mit “WebViewPage” durch unsere Benutzerdefinierte ViewPage ersetzen.

 <!--<pages pageBaseType="System.Web.Mvc.WebViewPage">-->
 <pages pageBaseType="User.Web.UI.Helper.CastleWindsor.CustomDiWebViewPage">
   <namespaces>
     <add namespace="System.Web.Mvc" />
     <add namespace="System.Web.Mvc.Ajax" />
     <add namespace="System.Web.Mvc.Html" />
     <add namespace="System.Web.Optimization"/>
     <add namespace="System.Web.Routing" />
     <add namespace="User.Web.UI" />
     <add namespace="User.Global.Resources" />
     <add namespace="User.Web.Common.WebAttributes" />
   </namespaces>
 </pages>

Jetzt wird bei jedem Rendern einer Seite unsere “CustomDiWebViewPage” verwendet.

2. Im View selbst nicht @model verwenden sondern über @inherits den View angeben über den die Seite gerendert werden soll z.B.:

@inherits User.Web.UI.Helper.CastleWindsor.CustomDiWebViewPage<User.Web.Models.Account.LoginModel>

 

Im View selbst kann dann einfach auf die Lokalisierung zugegriffen werden

@Localizeable.GetLocalizedString(„Test Lokalisierung“)

So jetzt zum wichtigsten Schritt, die Dependency Injection. Leider ist es mit Castle Windsor und einer CustomControllerFactory (älterer Post von mir über Di und Castle Windsor) nicht möglich einen View wie oben beschrieben zu Injecten.

Out of the Box wird das ganze aber z.B. von ninject unterstützt und mit Hilfe des NuGet Packages:

https://www.nuget.org/packages/Ninject.MVC3

muss für die Dependency Injection für einen View nur noch unsere Implementierung dem Interface zugewiesen werden im ninject DiKernel und alles weitere Funktioniert durch die Implementierung von IDependencyResolver von ninject. Wenn ich noch ermitteln kann, wie das ganze auch mit Castle Windsor funktioniert, werde ich den Post entsprechend anpassen.

Quellen:

http://bradwilson.typepad.com/blog/2010/07/service-location-pt3-views.html

http://sachabarbs.wordpress.com/2011/02/05/mvc-dependency-injection-into-views/

http://weblogs.asp.net/scottgu/archive/2010/10/19/asp-net-mvc-3-new-model-directive-support-in-razor.aspx

ASP.NET MVC – Model Binding und Postback für eine Liste mit Einträgen


Wenn man eine Liste z.B. mit Personen hat und man möchte die Liste in einer Tabelle darstellen und alle Felder sollen bearbeitbar sein, dann ist hier die Lösung mit ASP.NET MVC wahrscheinlich viel einfacher als man zuerst denken würde. Zumindest war ich überrascht wie einfach das ganze doch ist, wenn man die “richtige” Schleife zum Anzeigen der Werte benutzt.

Denn wenn man das ganze mit Hilfe einer “for” Schleife und dem jeweils passenden Index für einen Listeneintrag verwendet, dann baut MVC bei einem Postback die Liste genauso wieder zusammen.

Als erstes benötigen wir eine Liste aus z.B: Personen die angezeigt werden soll, am Besten mit unterschiedlichen Datentypen um den Vorteil von “Html.EditorFor”  zu verdeutlichen.

public class Person
{
    [Display(Name = "Loginname")]
    public string Loginname { get; set; }

    [Display(Name = "Nachname")]
    public string Name { get; set; }

    [Display(Name = "Vorname")]
    public string Vorname { get; set; }

    [Display(Name = "Alter")]
    public int Age { get; set; }

    [Display(Name = "Geburtstag")]
    public DateTime Birthdate { get; set; }

    [Display(Name = "Ist männlich?")]
    public bool IsMale { get; set; }
}

Dann noch ein einfaches Model welches eine Liste von Personen enthält. (alles sehr einfach gehalten)

public class EditListItemsModel
{
    public List PersonenListe { get; set; }

    public EditListItemsModel()
    {
        PersonenListe = new List();
    }

    public void LoadListItems()
    {
        PersonenListe.Add(new Person() { Loginname = "SquadWuschel@gmx.de", Vorname = "Squad", Name = "Wuschel", Age = 32, Birthdate = DateTime.Now.AddYears(-32), IsMale = true});
        PersonenListe.Add(new Person() { Loginname = "Jackson@gmx.de", Vorname = "Jack", Name = "Son", Age = 24, Birthdate = DateTime.Now.AddYears(-24), IsMale = true});
        PersonenListe.Add(new Person() { Loginname = "Fridi@gmx.de", Vorname = "Fri", Name = "di", Age = 52, Birthdate = DateTime.Now.AddYears(-52), IsMale = false});
    }

}

Im Controller die passenden Funktionen anlegen zum Anzeigen des Views und zum Verarbeiten der Postback Werte

 public ActionResult EditListItems()
 {
     EditListItemsModel model = new EditListItemsModel();
     model.LoadListItems();

     return View(model);
 }

 [HttpPost]
 public ActionResult EditListItems(EditListItemsModel model)
 {
     //Die Postback Modeldaten verarbeiten

     return View(model);
 }

Im View selbst dann in ein Formular unsere Einträge hinzufügen und hier verwendet man eine “for” Schleife und greift bei jedem EditorFor und LabelFor auf den passenden Index der Liste zurück. Denn dann erstellt MVC im HTML für das jeweilige “Namens”-Tag des Elements z.B. den folgenden Eintrag für das Alter vom Eintrag mit dem Index 1 “EditItems[1].Age”, bei einem Postback kann .NET dann die passenden Einträge wieder der Liste zuordnen.

@using (Html.BeginForm("EditListItems", "Home", FormMethod.Post, new { @class = "form-horizontal" }))
{</pre>
@for (int i = 0; i 0) { } }
<table class="table table-hover">
<tbody>
<tr>
<td colspan="4"></td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].Loginname)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].Loginname)</td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].Vorname)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].Vorname)</td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].Name)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].Name)</td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].Age)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].Age)</td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].Birthdate)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].Birthdate)</td>
</tr>
<tr>
<td>@Html.LabelFor(p => p.PersonenListe[i].IsMale)</td>
<td>@Html.EditorFor(p => p.PersonenListe[i].IsMale)</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">Speichern</td>
</tr>
</tfoot>
</table>
<pre>}

Das ganze sieht dann im Browser z.B. folgendermaßen aus:

image

Beim “Speichern” wird dann der passende View im Controller aufgerufen und hier ist dann die Liste im Postbackmodel mit den hier eingetragenen Werten entsprechend gefüllt und kann verarbeitet werden.

SimpleMembership mit ASP.NET MVC4 und EF


Wenn es mal wieder schnell gehen soll und man nicht viel Zeit in eine eigene Benutzerverwaltung investieren möchte, dann ist man beim SimpleMembership Provider für ASP.NET Projekte genau richtig.

Aber was macht bzw. ist der SimpleMembership Provider eigentlich?

Wenn man eine neue ASP.NET MVC4 Webseite für das Internet erstellt, dann ist hier bereits der SimpleMembership Provider “enthalten” und muss nur noch entsprechend konfiguriert werden. Dabei stellt der Provider bereits fertige Funktionen wie “WebSecurity.CreateUserAndAccount”, “WebSecurity.Login”, … zur Verfügung. Der Provider erstellt beim erstmaligen Ausführen eigene Tabellen in der angegebenen Datenbank und verwaltet damit die passenden Passwörter, Rollen, … Damit ist eine einfache Nutzerverwaltung innerhalb von ein paar Minuten konfiguriert und man hat Zugriff auf alle wichtigen Funktionen die man für eine einfache Benutzerverwaltung benötigt.

Man muss bei älteren Webprojekten zusätzlich aufpassen, denn es gibt einen “alten” MemberShip Provider aus ASP.NET 2.0 Zeiten welcher anders implementiert wird, hier muss man die Datenbanktabellen z.B. über ein extra Tool anlegen, außerdem wird kein OAuth unterstützt.

Einen sehr ausführlichen Artikel zum Thema SimpleMembership hat z.B. Jon Galloway geschrieben, dieser Artikel verschafft einen sehr guten Überblick über die Thematik.

Einbinden des SimpleMembership Providers in ein ASP.NET MVC Projekt mit EF

Im Folgenden will ich kurz erläutern wie man den bereits “fertigen” SimplemMembership Provider in einem MVC4 Projekt mit EF einrichtet und einige “unnötige” Abhängigkeiten wie einen zusätzlichen DB Context entfernt.

Dazu erstellen wir zuerst ein neues ASP.NET MVC4 Internet Projekt.

image

In dem neu erstellten ASP.NET MVC Projekt gibt es unter Filters die Datei “InitializeSimpleMembershipAttribute.cs” hier findet man den folgenden Aufruf:


 WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);

In diesen Aufruf tragen wir später unseren passenden Connectionstring und die Tabelle sowie die Spaltennamen für UserID und UserName ein.

image

Als nächstes legen wir erst einmal ein EF5 Model an in einer extra Klassenbibliothek an

image

Dafür wählen wir ein leeres Model

image

und erstellen eine neue Entität für das Model, den “Member” dieser wird später benötigt um unserem SimpleMembership Provider zu sagen wo der Username und die passende UserId stehen.

image

Da ich in meinen Projekten nie auf den eingebauten Connectionstring zurückgreife, sondern diesen lieber selbst “zusammenbaue” fügen wir unserer Klassenbibliothek in der sich das EF Model befindet noch eine weitere Datei “MemberModelContainer.cs” hinzu die unseren Context um einen weiteren Konstruktor erweitert. Diesem Konstruktor können wir direkt einen ConnectionString übergeben, den wir selbst zusammengebaut haben.

image

public partial class MemberModelContainer
{
    public MemberModelContainer(string connectionString) : base(connectionString)
    {
    }
}

Jetzt fügen wir unserer Webseite einen Verweis auf unser neues Projekt “Member.EF.Model” hinzu und erweitern die Web.config unter den appSettings um die folgenden Werte:


Damit wir jetzt überall auch auf unser Model zugreifen können, erstellen wir eine weitere Helper Klasse “CurrentHttpContext” die uns für den jeweiligen Request das aktuelle EF Model zur Verfügung stellt, mit den angegebenen Verbindungsdaten aus der Web.config.

public class CurrentHttpContext
{
    #region Properties
    private static string m_httpContextName = "EFDataModelHttpContextName";
    #endregion

    ///
    /// Gibt den aktuellen DataModelContainer für diesen Request zurück.
    ///
    public static MemberModelContainer GetDataModel()
    {
         //Den aktuellen dataContext nur für den aktuellen Request speichern
        if (HttpContext.Current.Items[m_httpContextName] == null)
        {
            //Connectionstring wird anhand des ModelNamens aus der web.config geladen
            MemberModelContainer efModelContainer = new MemberModelContainer(GetConnectionString("MemberModel"));
            HttpContext.Current.Items.Add(m_httpContextName, efModelContainer);
        }

        return (MemberModelContainer)HttpContext.Current.Items[m_httpContextName];
    }

    ///
    /// Gibt den Connectionstring zur DB zurück für das EntityFramework
    ///
    public static string GetConnectionString(string modelName)
    {
        //Initialize the EntityConnectionStringBuilder.
        EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder();
        //Providernamen setzten
        entityBuilder.Provider = "System.Data.SqlClient";
        //Connectionstring setzten
        entityBuilder.ProviderConnectionString = GetSqlBuilder().ToString();
        //Metadaten setzten.
        entityBuilder.Metadata = string.Format(@"res://*/{0}.csdl|res://*/{0}.ssdl|res://*/{0}.msl", modelName);
        return entityBuilder.ConnectionString;
    }

    public static SqlConnectionStringBuilder GetSqlBuilder()
    {
        SqlConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder();
        //Die Zugangsdaten aus der Web.Config laden, nicht den Connectionstring verwenden.
        sqlBuilder.DataSource = ConfigurationManager.AppSettings["DBDataSource"]; ;
        sqlBuilder.InitialCatalog = ConfigurationManager.AppSettings["DBCatalog"]; ;
        sqlBuilder.UserID = ConfigurationManager.AppSettings["DBUser"];
        sqlBuilder.Password = ConfigurationManager.AppSettings["DBPassword"];
        //Wichtige Option damit mehrere Abfragen geöffnet werden können
        //um Subabfragen ausführen zu können von Untergordneten Projekten
        sqlBuilder.MultipleActiveResultSets = true;
        sqlBuilder.IntegratedSecurity = false;
        //Dient zur Identifizierung der DB Connection/Application wenn man z.B: Fehler auf dem DB Server sucht.
        sqlBuilder.ApplicationName = "MemberManagement";

        return sqlBuilder;
    }
}

Jetzt können wir überall auf unsere eigenen Modeldaten zugreifen. Als nächstes erstellen wir die passende Datenbank aus unseren Modeldaten. Um unsere DB zu erstellen, fügen wir einfach in der Global.asax unter “Application_Start” folgendes hinzu:

MemberModelContainer container = new MemberModelContainer(CurrentHttpContext.GetConnectionString("MemberModel"));
container.Database.CreateIfNotExists();

(Privat nutze ich diese Funktion nur wenn ich entwickle, im Livebetrieb erstelle ich mir das passende SQL Script über das edmx Model und erstelle darüber meine Datenbank einmalig.)

Jetzt konfigurieren wir noch die Standardeinstellungen des SimpleMembership Providers und entfernen Code der nicht benötigt wird.

Als erstes bearbeiten wir die Datei “InitializeSimpleMembershipAttribute.cs”, hier wird einiges in die Global.asax ausgelagert, dazu gehört auch der Funktionsaufruf “LazyInitializer.EnsureInitialized”, denn diese Zeile muss nur einmal ausgeführt werden und dies lässt sich in der Global.asax im Application_Start am besten umsetzten.

Damit sieht die “InitializeSimpleMembershipAttribute.cs” am Ende folgendermaßen aus:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
{
    public class SimpleMembershipInitializer
    {
        public SimpleMembershipInitializer()
        {
            try
            {
                  WebSecurity.InitializeDatabaseConnection(CurrentHttpContext.GetSqlBuilder().ConnectionString, "System.Data.SqlClient", "MemberSet", "Id", "Username", autoCreateTables: true);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
            }
        }
    }
}

Wichtig ist in der Funktion “SimpleMembershipInitializer” der Aufruf für

WebSecurity.InitializeDatabaseConnection(CurrentHttpContext.GetSqlBuilder().ConnectionString, „System.Data.SqlClient“, „MemberSet“, „Id“, „Username“, autoCreateTables: true);” Hier wird unser Connectionstring angegeben, sowie der Name der Tabelle in der die Userdaten gespeichert werden und die Spalte mit der “UserId” sowie mit dem “Benutzernamen”. Diese Funktion muss entsprechend eurer eigenen Modeldaten angepasst werden. Außerdem erstellt, diese Funktion auch die passenden DB Tabellen die der SimpleMembership Provider benötigt.

Aufgrund dieser Informationen, ist der Provider dann in der Lage alle wichtigen Funktionen wie Login, Logout, Register, … uns in einem einfachen Aufruf zur Verfügung zu stellen.

Über die Klasse “WebSecurity” haben wir Zugriff auf alle wichtigen Account Funktionen und über die Klasse “Roles” haben wir Zugriff auf die Rollenverwaltung für die User, die der SimpleMembership Provider ebenfalls enthält.

Die angepasste Global.asax sieht dann folgendermaßen aus:

public class MvcApplication : System.Web.HttpApplication
{
    private static InitializeSimpleMembershipAttribute.SimpleMembershipInitializer _initializer;
    private static object _initializerLock = new object();
    private static bool _isInitialized;

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterAuth();

        MemberModelContainer container = new MemberModelContainer(CurrentHttpContext.GetConnectionString("MemberModel"));
        container.Database.CreateIfNotExists();

        LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);

    }
}

Außerdem kann der folgende Code aus der “AccountModel.cs” entfernt werden, da wir unser eigenes Model und eigenen DB Context verwenden:

public class UsersContext : DbContext
 {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet UserProfiles { get; set; }
  }

    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

In der “AccountController.cs” muss noch die Funktion “ExternalLoginConfirmation” auf unser Model angepasst werden

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLoginConfirmation(RegisterExternalLoginModel model, string returnUrl)
{
    string provider = null;
    string providerUserId = null;

    if (User.Identity.IsAuthenticated || !OAuthWebSecurity.TryDeserializeProviderUserId(model.ExternalLoginData, out provider, out providerUserId))
    {
        return RedirectToAction("Manage");
    }

    if (ModelState.IsValid)
    {
        // Insert a new user into the database
        using (MemberModelContainer db = CurrentHttpContext.GetDataModel())
        {
            var user = db.MemberSet.FirstOrDefault(u => u.Username.ToLower() == model.UserName.ToLower());
            // Check if user already exists
            if (user == null)
            {
                // Insert name into the profile table
                db.MemberSet.Add(new global::Member.EF.Model.Member() { Username  = model.UserName, Vorname = "", Nachname = ""});
                db.SaveChanges();

                OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.UserName);
                OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);

                return RedirectToLocal(returnUrl);
            }
            else
            {
                ModelState.AddModelError("UserName", "User name already exists. Please enter a different user name.");
            }
        }
    }

    ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(provider).DisplayName;
    ViewBag.ReturnUrl = returnUrl;
    return View(model);
}

Und die Registerfunktion muss ebenfalls angepasst werden, denn hier muss der Vorname und Nachname mit in der DB eingetragen werden, sonst kommt es zu einem Fehler beim Anlegen des Users.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        try
        {
            //TODO  RegisterModel und View um Vorname und Nachname erweitern, dann kann dies hier mit ausgfüllt werden.
            WebSecurity.CreateUserAndAccount(model.UserName, model.Password, new { Vorname = "TestVorname", Nachname="TestNachname" });
            WebSecurity.Login(model.UserName, model.Password);
            return RedirectToAction("Index", "Home");
        }
        catch (MembershipCreateUserException e)
        {
            ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Außerdem kann das Attribut “[InitializeSimpleMembership]” über der AccountController Klasse entfernt werden, denn die Initialisierung des SimpleMembership Providers, findet jetzt in der Global.asax statt.

Wenn wir jetzt unsere Webseite entsprechend ausführen und einen neuen User Registrieren, sieht unsere Datenbank dann folgendermaßen aus:

image

Die Tabelle “dbo.MemberSet” wurde von unserem EF Model angelegt und die restlichen Tabellen vom SimpleMembership Provider.

Dipendency Injection und TDD mit dem SimpleMembership Provider

Da der SimpleMembership Provider nicht über einen Konstruktor initialisiert werden kann, ist es nicht möglich diesen per DI zu initialisieren. Da ich versuche in meinen aktuellen Projekten auf DI zu setzten, überlege ich aktuell ob ich wirklich auf den SimpleMembership Provider setzte oder einfach ein eigenes UserRepository erstelle, wer hier bereits entsprechende Erfahrungen gemacht hat, würde ich mich über einen Kommentar freuen.

Um den SimpleMembership Provider zu testen, habe ich nur den folgenden Artikel gefunden, der angibt wie man den SimpleMembership Provider zumindest “testen” kann.

Auch den folgenden Artikel darüber ob man auf den SimpleMembership Provider setzten sollte fand ich zumindest sehr informativ.

Das fertige Projekt kann wie immer über meinen Codeplex Account angeschaut werden unter:

MVC –> SimpleMembershipProvider

MVC Custom Validation mit IValidatableObject Interface


Letzte Woche wurde ich auf ein Interface aufmerksam gemacht, welches ich bisher in MVC noch nicht benutzt habe. Dabei handelt es sich um das IValidatableObject Interface, mit diesem Interface kann man sehr einfach alle möglichen und komplexen Validierungen für sein Model einbinden. Die Daten können dann an geeigneter Stelle mit “ModelState.IsValid” geprüft werden.

Dafür muss im Model nur das Interface “IValidatableObject” eingebunden werden und mit dem Interface noch die dazu verbundene Funktion “Validate”. In dieser Funktion werden die Daten nach den eigenen Vorstellungen geprüft, nach Abfragen die die Standardattribute wie “Required”, “MinLength”, …nicht abdecken. Dazu gehört z.B. der Vergleich zweier Datumswerte oder Strings. Dazu lassen sich dann Benutzerdefinierte Fehlermeldungen angeben und bei welchen Eingabefeldern diese Fehlermeldungen angezeigt werden sollen.

public class CustomValidationModel : IValidatableObject
{
    [Required]
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime Birthdate { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime Deathdate { get; set; }

    [Required]
    public int MinValue { get; set; }

    [Required]
    public int MaxValue { get; set; }

    public void Initilize()
    {
        Birthdate = DateTime.Now.Date;
        Deathdate = DateTime.Now.Date.AddYears(100);
    }

    /// 
    /// Die Custom Validate Methode die durch das Interface eingebunden wird.
    /// 
    public IEnumerable Validate(ValidationContext validationContext)
    {
        if (Birthdate.Date < DateTime.Now)
        {
            yield return new ValidationResult("Das Geburtsdatum muss in der Zukunft liegen", new[] { "Birthdate" });
        }

        if (MinValue  10 && MinValue > 10)
        {
            //Ausgeben der passenden Fehlermeldung und angeben bei welchen Werten die Fehlermeldung angezeigt werden soll
            yield return new ValidationResult("MaxValue und MinValue dürfen nicht größer wie 10 sein.", new[] { "MinValue", "MaxValue" });
        }

        if (Name.Trim().Length > 3 && !Name.StartsWith("Sq"))
        {
            yield return new ValidationResult("Wenn der Name länger wie 3 Zeichen ist, muss dieser mit 'Sq' beginnen.", new[] { "Name" });
        }
    }
}

Dazu muss im Controller nur noch “ModelState.IsValid” geprüft werden und schon haben wir unsere eigene Validierung eingebunden.

public ActionResult CustomValidation()
{
    CustomValidationModel model = new CustomValidationModel();
    model.Initilize();
    return View("CustomValidation", model);
}

[HttpPost]
public ActionResult CustomValidation(CustomValidationModel model)
{
    if (ModelState.IsValid)
    {
        //TODO Valides Model - Aufgaben ausführen
    }

    return View("CustomValidation", model);
}

Im View selbst können die Fehlermeldungen wie gewohnt direkt über “Html.ValidationMessageFor()” bei jedem Eingabefeld ausgegeben werden oder über das “Html.ValidationSummary()” global angezeigt werden.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Name, new { @class = "control-label" })
<div class="controls">@Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name)</div>
</div>
<pre>

 

Der Nachteil an dieser Lösung ist, das leider keine direkte Möglichkeit besteht die Serverseitige Validierung mit der Clientseitigen Validierung zu verbinden. Wer also Serverseitige und Clientseitige Validierung in MVC mit Bordmitteln verbinden möchte, der sollte sich zum ValidationAttribute und dem zugehörigen Interface IClientValidatable z.B. hier belesen.

Codeplex:

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier findet Ihr dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/CustomValidation”, “Models/HomeModels/CustomValidationModel”.