Archiv der Kategorie: WebApi

ASP.NET MVC und WebAPI über Dependency Injection Instanziieren (Castle Windsor) – Part 2


Wie ich bereits in meinem ersten Artikel zur Serie TDD erwähnt habe, verwende ich das Dependency Injection (DI) Framework Castle Windsor mit ASP.NET MVC um meine Controller entsprechend zu instanziieren.

Bei der Anwendung von DI mit ASP.NET MVC muss man unterscheiden zwischen den Standard Controllern (DefaultControllerFactory) und den WebAPI Controllern (IHttpControllerActivator). Daher ist für den jeweiligen Controller-Typ ist eine eigene Implementierung notwendig.

Was macht das DI Framework eigentlich für uns?

Anhand von Regeln, die man dem DI Container hinterlegt, kann das DI Framework selbstständig Instanzen von Klassen erstellen. Aufgrund dessen, gibt es im Programm nur noch eine zentrale Stelle an der Instanzen von Klassen erstellt werden. Auch hier gibt es Ausnahmen, dazu gehören z.B. einfache Datenhaltungsklassen. Nichtdestotrotz muss jeweils anhand des vorliegenden Problems geklärt werden, ob eine Instanz über DI übergeben werden muss oder evtl. lokal instanziiert werden kann.

In einem ASP.NET MVC Projekt befindet sich die zentrale Stelle für das Einbinden von Klasseninstanzen beim Erstellen eines Controllers. Da der Standard Controller aber parameterlos ist, muss im jeweiligen Controller einfach ein Konstruktor mit den gewünschten Parametern erstellt werden. Damit dieser Konstruktor dann auch aufgerufen und mit den passenden Abhängigkeiten instanziiert wird, muss das Erstellen der Controller von uns überschrieben werden. Das Instanziieren übernimmt dann Castle Windsor für uns, mit den von uns hinterlegten Regeln.

1. ASP.NET MVC DI mit Castle Windsor initialisieren

Wie bereits erwähnt, benötigen wir einen Castle Windsor Container indem wir unsere Regeln für das Instanziieren von Klassen, z.B. Controllern ablegen. Das Ganze muss in der Global.asax im “Application_Start” entsprechend aufgerufen werden.

private static IWindsorContainer Container { get; set; }

protected void Application_Start()
{
    ...
    //Erstellen unserer Castle Windsor Instanz die wir im gesamten Projekt benutzten
    Container = new WindsorContainer();
    //Hinzufügen unserer Konfigurationen zum Kontainer.
    Container.Install(new WebInstaller(connectionString), new LoggingInstaller());
    ...
}

Über den so genannten Installer von Castle Windsor können wir unsere einzelnen Regelpakete für den Container hinzufügen (registrieren). Der WebInstaller sieht z.B in Auszügen folgendermaßen aus und bezieht sich auf eines meiner Projekte. Wie die Regeln genau aussehen können und was zu beachten ist, kann man auch sehr gut in der Castle Windsor Dokumentation nachlesen.

///
/// Installer für alle wichtigen Webverweise
///
public class WebInstaller : IWindsorInstaller
{
    private  string ConnectionString { get; set; }

    ///
    /// Initialize WebInstaller
    ///
    /// Connectionstring übergeben, der für den Webinstaller benötigt wird.
    public WebInstaller(string connectionString)
    {
        ConnectionString = connectionString;
    }

    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        //Unseren EFModelContainer registrieren mit dem passenden Connectionstring dazu, ACHTUNG wir müssen die bereits erstellte Instanz verwenden,
        //da wir darauf auch bei einigen anderen statischen "Variablen" zugreifen, z.B. Rechteverwaltung und Co.
        container.Register(Component.For(typeof(MainModelContainer)).Instance(CurrentHttpContext.GetDataModel()).LifestylePerWebRequest());

        //Alle Queries Registrieren die sich in der Assembly "User.EF.Queries" befinden und jeweils ein passendes Interface enthalten wie
        //"IMemberQueries/MemberQueries" siehe auch: http://docs.castleproject.org/Windsor.Registering-components-by-conventions.ashx
         container.Register(Classes.FromAssemblyContaining().InNamespace("User.EF.Queries").WithService.DefaultInterfaces().LifestylePerWebRequest());

        //Alle Controller Registrieren die IController beinhalten/nutzen.
        container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());
        //Alle WebAPI Controller verwenden IHttpController
        container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());
        ....
      }
}

Nach dem Castle Windsor soweit vorbereitet ist und unseren Controller entsprechend instanziieren kann, benötigen wir unsere eigene ControllerFactory. Dafür muss eine neue Klasse erstellt werden, die von “DefaultControllerFactory” ableitet und der wir unseren Castle Windsor Container in einem eigenen Konstruktor übergeben können. Mit Hilfe von “this._container.Resolve(controllerType)” entscheidet Castle Windsor selbstständig welcher Controller aufgerufen wird und welche Abhängigkeiten erstellt sowie instanziiert werden müssen.

 public class CustomControllerFactory : DefaultControllerFactory
 {
     private readonly IWindsorContainer _container;

     ///
     /// Den Windsor Container für unser Projekt übergeben
     ///
     /// Unser aktueller Windsor Kontainer
     public CustomControllerFactory(IWindsorContainer container)
     {
         if (container == null)
         {
             throw new NullReferenceException("IWindsorContainer is Null");
         }

         _container = container;
     }

     ///
     /// Erstellen der passenden Controller Instanzen anhand unseres Containers werden die Instanzen automatisch aufgelöst.
     ///
     protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
     {
         if (controllerType == null)
         {
             throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
         }

         //Unseren Controller auflösen per Castle Windsor
         var controller = (IController)this._container.Resolve(controllerType);

         return controller;
     }

     public override void ReleaseController(IController controller)
     {
         //Wenn der Controller "aufgelöst werden soll, dann auch den Controller aus dem Context entfernen.
         if (HttpContext.Current.Items.Contains(CurrentControllerIndexName))
         {
             HttpContext.Current.Items.Remove(CurrentControllerIndexName);
         }

         this._container.Release(controller);
     }
 }

Damit unsere neue Factory zum Erstellen von Controllern auch von ASP.NET MVC “gefunden” bzw. versendet werden kann, muss diese noch registriert werden. Dies geschieht auch in der global.asax im “Application_Start”

//Erstellen unserer CustomControllerFactory zum Instanziieren der Passenden Controller
var customControllerFactory = new CustomControllerFactory(Container);
//Unsere CustomControllerFactory als Standard Factory zum Erstellen von Controllern setzten, damit diese auch von MVC verwendet wird.
ControllerBuilder.Current.SetControllerFactory(customControllerFactory);

Wenn soweit alles eingestellt ist, kann ein MVC Controller z.B. auch folgenden Konstruktor besitzen, denn durch Castle Windsor werden alle Abhängigkeiten entsprechend aufgelöst und instanziiert übergeben.

 [CustomAuthorize]
 public class AccountController : ControllerBaseUser
 {
     #region Member
     ///
     /// Unser Repository für die Mitarbeiterabfragen
     ///
     private readonly IDiMembership _membership;

     ///
     /// User Repository für alle allgemeinen Abfragen fürs Web.
     ///
     private readonly IWebQuerySummary _webQueries;

     ///
     /// Mailverwaltung für die Accountverwaltung
     ///
     private readonly IAccountMails _accountMails;
     #endregion

     #region Initialize
     public AccountController(IDiMembership membership, IAccountMails accountMails, IWebQuerySummary webQueries)
     {
         if (membership == null)
         {
             throw new NullReferenceException("IDiMembership is Null");
         }

         if (accountMails == null)
         {
             throw new NullReferenceException("AccountMails is Null");
         }

         if (webQueries == null)
         {
             throw new NullReferenceException("WebQuery is Null");
         }

         _webQueries = webQueries;
         _accountMails = accountMails;
         _membership = membership;
     }
     #endregion

2. ASP.NET WebAPI mit Castle Windsor initialisieren

Leider funktioniert die DI der Standard Controller nicht bei den WebAPI Controllern. Damit auch die WebAPI Controller mit DI instanziiert werden können sind die folgenden Schritte notwendig.

Dabei kann natürlich auch in der WebAPI auf die gleichen DI Regeln wie bei den Standard Controllern zurückgegriffen werden. Wir müssen nur im WebInstaller aufpassen, dass wir auch eine Regel für die WebAPI Controller angeben, damit diese aufgelöst werden können.

//Alle WebAPI Controller verwenden IHttpController
container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());

Dann kann auch der bereits angelegte Container für die Standard Controller auch für die WebAPI verwendet werden.

Die WebAPI benötigt eine eigene Controller Klasse, der wir unseren DI Container übergeben können und in der dann die WebAPI Controller entsprechend aufgelöst werden können.

///
/// Um die webApi auch über IoC nutzen zu können ist folgender Code notwendig.
/// Und dann muss das ganze über "GlobalConfiguration.Configuration" in der Global Assax registriert werden.
/// http://www.cafe-encounter.net/p1316/windsor-di-with-asp-net-webapi
///
public class CustomWebApiController : IHttpControllerActivator
{
    private readonly IWindsorContainer _container;

    public CustomWebApiController(IWindsorContainer container)
    {
        this._container = container;
    }

    public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        var controller = (IHttpController)this._container.Resolve(controllerType);
        request.RegisterForDispose(new Release(() => this._container.Release(controller)));
        return controller;
    }

    private class Release : IDisposable
    {
        private readonly Action release;

        public Release(Action release)
        {
            this.release = release;
        }

        public void Dispose()
        {
            this.release();
        }
    }
}

Jetzt muss, wie bereits bei den Standard Controllern, unsere neue Klasse nur noch registriert werden, so dass sie die WebAPI Controller Instanzen anlegen kann. Dazu wird wieder in der Global.asax in der “Application_Start” die entsprechende Codezeile eingefügt.

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new CustomWebApiController(Container));

Und schon kann unser WebAPI Controller den folgenden Konstruktor enthalten.

 public class AdminApiController : ApiController
 {
     #region Member
     ///
     /// User Repository für alle allgemeinen Abfragen fürs Web.
     ///
     private readonly IWebQuerySummary _webQueries;
     #endregion

     #region Initialize
     public AdminApiController(IWebQuerySummary webQueries)
     {
         if (webQueries == null)
         {
             throw new NullReferenceException("WebQuery is Null");
         }

         _webQueries = webQueries;
     }
     #endregion
}

CodePlex

Mein bereits angefangenes Projekt findet Ihr wie immer unter Codeplex –> DiTemplates –> UserManagement

Quellen

http://www.cafe-encounter.net/p1316/windsor-di-with-asp-net-webapi

http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container

http://docs.castleproject.org/Windsor.Registering-components-by-conventions.ashx

MVC 4 Web API Custom Authorize Attribut


Wie bereits in meinem letzten Artikel erwähnt, benötigt man für Web API ein eigenes Authorize Attribut aus einem anderen .NET Namespace. Das hier verwendete Authorize Attribut kommt aus dem Namespace “System.Web.Http.AuthorizeAttribute”.

Auch hier bietet Microsoft wieder eine Standardlösung an, die nur prüft ob der User eingeloggt ist, dafür muss wie bisher gewohnt im Web API Controller das Authorize Attribut aus dem oben genannten Namespace verwendet werden.

Gibt man mit seinen Web API Aufrufen aber Nutzerspezifische Daten zurück, die nicht jeder eingeloggte User abrufen darf, wird hier wieder ein eigenes Authorize Attribut benötigt. Hier handelt es sich um den gleichen Aufbau wie beim normalen Authorize Attribut. Man kann wieder auf den Controler, Action, Id und sonstige Routing Informationen zugreifen und dann entsprechend im Custom Authorize Attribut darauf reagieren. Damit man weiß welcher User eingeloggt ist muss auch hier wieder das “FormsAuthentication.SetAuthCookie” beim normalen Seitenlogin benutzt werden.

Ich möchte zusätzlich noch darauf hinweisen, das ich für meine Web API Aufrufe nicht die Standardroute verwende. Damit wir die Web API auch so nutzen können wie wir es bisher von den MVC Controllern gewohnt sind, müssen wir noch eine weitere Route zu unserer WebApi hinzufügen. Denn mit der Standardroute für API aufrufe sind nur GET/POST/DELETE Abfragen möglich. Aber meist hat man mehr wie nur diese “paar” Abfragen die man für einen View benötigt, daher habe ich noch eine neue Route vor der “DefaultApi” Route hinzugefügt.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Registrieren unseres Filters für die Modelvalidation für AJAX Requests
        config.Filters.Add(new ValidateAttribute());

        //Alternatives Routing, hier ist es jetzt wieder möglich gezielte Actionen und Controller abzufragen
        //In der DefaultApi kann man nur Get/Post/Delete abfragen und ist damit sehr begrenzt was die Abfragen angeht.
        config.Routes.MapHttpRoute(
            name: "AlternateWebApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        //Standardroute  - hier lässt sich nur GET/POST/DELETE abfragen.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Unser Benutzerdefiniertes Web API Authorize Attribut sieht dann folgendermaßen aus und hier darauf achten das man das richtige Authorize Attribut aus dem richtigen Namespace einbindet!

using AuthorizeAttribute = System.Web.Http.AuthorizeAttribute;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomWebApiAuthorizeAttribute : AuthorizeAttribute
{
    #region Member
    private HttpActionContext CurrentContext { get; set; }
    #endregion

    /// <summary>
    /// Prüfen ob der User auch Autorisiert ist, auf die passenden API Aufrufe zuzugreifen
    /// </summary>
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        //Wenn der User nicht eingeloggt ist, dann wird er hier schon mit einem 401 abgewiesen
        base.OnAuthorization(actionContext);

        //Der Filtercontext wird benötigt um auf die RequestDaten zuzugreifen, z.b. auf die UserId die zugegriffen
        CurrentContext = actionContext;
       

        //Wenn kein User eingeloggt ist, dann hat er auch keinen Zugriff.
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            //Wenn der User nicht eingeloggt ist, auf die Loginseite verweisen
            HandleUnauthorizedRequest(actionContext);
        }
        else
        {
            //Prüfen der passenden Rechte für die einzelnen Actions/Methoden
            //Wenn der User nicht für den View/Action authentifiziert ist, dann auf die Loginseite verweisen.
            if (!CheckRights())
            {
                HandleUnauthorizedRequest(actionContext);
            }
        }

        //Wenn alles i.o. ist "nichts" unternehmen und einfach beim Aufbau der Seite weitermachen.
    }

    /// <summary>
    /// Den unautorisierten Eingriff, "abwehren"
    /// </summary>
    /// <param name="actionContext"></param>
    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        throw new HttpResponseException(challengeMessage);
    }

    #region Private Functions
    /// <summary>
    /// Prüfen um welchen CurrentController es sich handelt und die passende Sicherheitsprüfung vornehmen
    /// Hier werden Nur API Controller überprüft
    /// </summary>
    /// <returns>TRUE->Darf zugreifen | FALSE->Darf nicht zugreifen</returns>
    private bool CheckRights()
    {
        //Setzen des aktuellen Actionnamen, der aufgerufen wird.
        string actionName = CurrentContext.ActionDescriptor.ActionName;
        //Laden des Controllernamens über den der API Aufruf kam
        string controllerName = CurrentContext.ControllerContext.ControllerDescriptor.ControllerName;
        //Auslesen der ID, wenn eine in der URL mit übergeben wurde.
        string id = CurrentContext.ControllerContext.RouteData.Values.ContainsKey("id") ?
                    CurrentContext.ControllerContext.RouteData.Values["id"].ToString() : null;

        //Den aktuellen User ermitteln der den API Aufruf gestartet hat.
        string username = HttpContext.Current.User.Identity.Name;

        //TODO Prüfung vornehmen ob der Aktuelle User auf die ermittelten Resourcen zugreifen darf.

        return true;
    }
    #endregion
}

Das Attribut kann dann wieder problemlos über dem passenden ApiController verwendet werden

[CustomWebApiAuthorize]
public class WorktimeApiController : ApiController
{
    /// <summary>
    /// Filtert unsere Arbeitszeiteinträge nach den letzten Einträgen die vorgenommen wurde.
    /// </summary>
    /// <param name="id">Die zugehörige UserId zu der die Einträge gefiltert werden sollen</param>
    /// <param name="value">Unsere Modeldaten nach denen gefiltert werden soll</param>
    public HttpResponseMessage ByEntryFilter(long id, WorktimeEntryFilterModel value)
    {
        if (value != null)
        {
            WorktimeListModel worktime = new WorktimeListModel(id, value);
            //Den passen Listentyp speichern, wenn der jeweilige Filter ausgewählt wurde.
            worktime.SaveDefaultListType(id, EWorktimeListTypes.ByEntries);
            worktime.FilterWorktimeList();

            //Wird automatisch in JSON Objekt umgewandelt und gibt den StatusCode 200 für OK zurück!
            HttpResponseMessage response = Request.CreateResponse<WorktimeListModel>(HttpStatusCode.OK, worktime);
            return response;
        }

        //Statuscode 400 Zurückgeben
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

...

MVC WebAPI und Serialisieren von Eingabedaten ohne Form Submit


Die Tage hatte ich das Problem, dass ich auf einer Seite noch ein paar Eingabefelder für einen Filter unterbringen musste. Es war mir leider nicht möglich ein zweites Form Tag dafür zu erstellen, daher habe ich die Elemente mit in das Hauptformular übernommen.

Ich benötigte für meinen Ajax Request an die WebAPI aber nur die Eingabefelder für meinen Filter und nicht die gesamten Formulardaten und ich wollte auch nicht jedes Eingabefeld einzeln heraussuchen und die Werte per Hand zusammensetzten.

Wenn man dann weiß das man mit jQuery problemlos Formulare Serialisieren kann und man kurz im Internet sucht, findet man auch schnell eine Möglichkeit wie man nur bestimmte Eingabefelder serialisieren kann ohne dabei das ganze Formular zu nutzen. Ich habe einfach um meine Suchfelder ein DIV mit einer ID gesetzt und dann per jQuery und einem passenden Selector die passenden Daten erhalten.

<input id="currentDate" class="span35" type="date" name="currentDate">

<div id="searchDiv" class="well well-small">
  <div class="btn-block">
    <select id="FilterWorktimeTypeList" class="span42" name="FilterWorktimeTypeList">
   </div>
   <div class="input-append btn-block">
     <input id="filterText" class="span35" type="text" name="SearchText">
     <button id="filterBtn" class="btn" type="button">Filtern</button>
  </div>
</div>

Da currentDate nicht innerhalb unseres SuchDIVs liegt, müssen wir den Inhalt, danach per Hand zu unseren Daten hinzufügen. Wichtig ist, das jedes Eingabefeld einen Namen hat, denn sonst wird es nicht serialisieren. Außerdem kann nicht die Hausmethode von jQuery zum serialisieren verwendet werden, sondern die Funktion “serializeObject” muss verwendet werden und erst in Verbindung mit “JSON.stringify” wird unser Datenobjekt auch von der WebAPI “erkannt”.

//wir suchen alle Inputfelder unterhalb unseres Divs heraus.
var searchDiv = $("#searchDiv :input")
//Wir serealisieren erst unser "Formular" aka unser SuchDiv
var jsonData = searchDiv.serializeObject();
//Da im Suchdiv nicht unser Datum mit enthalten ist, fügen wir es unserem Objekt mit hinzu.
jsonData["Datum"] = $("#currentDate").val();
//Wir müssen unsere serealisieren Daten noch so umwandeln, das diese auch von der WebAPI erkannt werden
jsonData = JSON.stringify(jsonData);

Die Funktion “serializeObject” habe ich im Netz gefunden und funktioniert wunderbar. Die Funktion selbst greift auf die jQuery Funktion “serializeArray” zurück. Diese Standardfunktion reicht leider nicht aus um ein gültiges Objekt zu erstellen, was die WebAPI als Objekt erkennt und umwandelt.

//jQuery Plugin um z.B. direkt Inputfelder und derren Inhalt zu serealisieren als Objekt!
//http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery
$.fn.serializeObject = function()
{
    var o = {};
    var a = this.serializeArray();
    $.each(a, function() {
        if (o[this.name] !== undefined) {
            if (!o[this.name].push) {
                o[this.name] = [o[this.name]];
            }
            o[this.name].push(this.value || '');
        } else {
            o[this.name] = this.value || '';
        }
    });
    return o;
};

Damit die Daten dann auch entsprechend an die WebAPI als Model übergeben werden können benötigen wir ein passendes C# Model für die Daten.

public class WorktimeEntryFilterModel
{
  /// <summary>
  /// Der Text nach dem gesucht werden soll.
  /// </summary>
  public string SearchText { get; set; }

  /// <summary>
  /// Die ausgewählte ID der WorktimeTypeList für die Filter
  /// </summary>
  public int? FilterWorktimeTypeList { get; set; }

  /// <summary>
  /// Der Datumsstring aus dem Model.
  /// </summary>
  public string Datum { get; set; }
}

Wichtig ist, das ich bei meinen WebAPI Aufrufen eine extra Route eingebunden habe in der WebApiConfig, wieso und weshalb das könnt Ihr hier in einem anderen Artikel nachlesen.

Der Beispielhafte Aufruf unserer Daten “jsonData” kann dann z.B. folgendermaßen aussehen.

$.ajax({
   url: "/api/worktimeApi/ByTextFilter/@Model.UserId",
   type: "POST",
   data: jsonData
   contentType: 'application/json; charset=utf-8',
   statusCode: {
        200 /* OK */: function(daten) {
            //Do some fun with the Data
            }
   }
});

und die passende WepAPI Funktion die unsere Daten entgegennimmt und verarbeitet.

/// <summary>
/// Filtert den Eintrag nach den übergebenen Modeldaten
/// </summary>
/// <param name="id">Die UserId nach der gefiltert werden soll</param>
/// <param name="value">Unsere Modeldaten nach denen gefiltert werden soll</param>
public HttpResponseMessage ByTextFilter(long id, WorktimeEntryFilterModel value)
{
    if (value != null)
    {
        WorktimeListModel worktime = new WorktimeListModel(id, value);
        worktime.FilterWorktimeList();

        //Wird automatisch in JSON Objekt umgewandelt und gibt den StatusCode 200 für OK zurück!
        HttpResponseMessage response = Request.CreateResponse<WorktimeListModel>(HttpStatusCode.OK, worktime);
        response.Headers.Location = new Uri(Request.RequestUri, string.Format("/api/WorktimeApi/ByTextFilter/{0}", id.ToString()));
        return response;
    }

    throw new HttpResponseException(HttpStatusCode.BadRequest);
}

Damit ist es problemlos möglich nur kleine Teile eines großen Formulars zu benutzten. Vielleicht existiert auch kein Formular und damit hat man die Möglichkeit die Daten trotzdem problemlos zu serialisieren und zu versendet.

MVC4 WebaAPI und knockoutjs – Teil 3 – Datum und Zeitangaben (moment.js)


Die größten Probleme hatte ich mit knockoutjs inkl. mapping Plugin bisher mit dem Zusammenspiel von MVC Datums und Zeitangaben. Hier habe ich kein Beispiel für meine “Problemstellung” im Netz finden können. Daher versuche ich im Folgenden zu erläutern welche Plugins ich verwendet habe um das Problem zu lösen.

1. Problemstellung

Verwendung von Modeltype “Date” und “Time” im C# Model welches im Formular abgebildet werden soll.

[DataType(DataType.Date)]
[Display(Name = "Geburtsdatum")]
public DateTime Birthdate { get; set; }

[DataType(DataType.Time)]
[Display(Name = "Startzeit")]
public DateTime Starttime { get; set; }

Das Erste Problem ist die Übertragung der Werte im JSON Format. Es muss darauf geachtet werden, das die Datumswerte im ISO-8601 Format umgewandelt werden. Die C# Lib von “Newtonsoft.Json” hilft hier weiter, denn diese verwendet Standardmäßig ISO-8601 bei Datumswerten.

//Umwandeln des Models in ein JSON Objekt mit JsonConvert - ISO Format für DateTime
var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

Damit die Eingabefelder für “Date” auch als Valides Datum erkannt werden habe ich bereits einen Blogpost erstellt, der hier ebenfalls beachtet werden muss.

Aktuell wird im Eingabefeld aber nur das Komplette ISO-8601 Datumsformat angezeigt

“2013-01-10T19:40:19.2353852+01:00”

Damit hier das richtige Datum dargestellt wird muss ein Bindinghandler für “Date” und “Time” erstellt werden. Hier ist es wichtig, das die Werte nicht nur im Browser richtig dargestellt werden, sondern das im knockoutjs Model weiterhin das Datum im ISO-8601 Format vorliegt, denn sonst kann das JSON-Objekt nicht automatisch in unser C# Model Objekt umgewandelt werden.

2. Verwendete JavaScript Addons

Alle Javascript Addons sind per NuGet verfügbar.

  • moment.js für das umwandeln und Berechnen sowie die Formatierte Ausgabe von Datumsangaben in JavaScript. Wie ich finde eine Sehr coole und nützliche Library.
  • knockoutjs für das Modelbinding
  • jQuery als Abhängigkeit und weil wir es auch verwenden.
  • jQuery-Globalize (siehe Blogpost von mir)

3. Umsetzung

Umgesetzt werden soll ein einfaches Formular mit Daten in denen auch die Datentypen (DataType) “Date” und “Time” mit dargestellt werden sollen in einer einfachen Textbox. Dieses Formular wird per Submit Button in einem AJAX Request an eine WebAPI Funktion übergeben werden die als Parameter den Modeltype enthält den wir im Formular dargestellt haben.

Erstellen eines passenden Models in C# welches den DataType “Date” und “Time” besitzt und erstellen eines Passenden Formulars (Views). Im View verwenden wir nicht “value” sondern die Namen unserer CustomModelBinder für knockoutjs.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Birthdate, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Birthdate, new { data_bind = "dateValue: Birthdate" }) @Html.ValidationMessageFor(model => model.Birthdate)</div>
</div>
<div class="control-group">@Html.LabelFor(model => model.Starttime, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Starttime, new { data_bind = "timeValue: Starttime" }) @Html.ValidationMessageFor(model => model.Starttime)</div>
</div>
<pre>

Wie bereits weiter oben beschrieben wandeln wir unser C# Model im View in ein JSON Objekt um und binden es an unser knockoutjs Model.

//Umwandeln des Models in ein JSON Objekt mit JsonConvert - ISO Format für DateTime
var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

Erstellen unseres bindingHandlers für den DataType “Date”. Wichtig beim Erstellen der Handler ist zum einen die Verwendung von “ko.bindingHandlers.value.init(…)” und “ko.bindingHandlers.value.update(…)” durch das Aufrufen dieser Funktion werden wohl die Standardevents für den “value” type auf auf meinen Handler angewendet und der Handler wird bei einer Wertänderung automatisch aufgerufen.

Außerdem wird mit Hilfe der Funktion “bindingContext.$data[element.id](date.format());” für unser aktuelles Element das ISO-8601 Datum im Model hinterlegt.

Beim Zurückschreiben des Datums zum Server wird hier die Lokale Client Uhrzeit übergeben, wenn die “alte” Originaluhrzeit wichtig ist, muss das Datum auf Serverseite demensprechend verarbeitet werden.

//Eigene bindinghandler für das Datumsformat für knockoutjs erstellen
ko.bindingHandlers.dateValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        //die ValueBindings Handler von Knockout verwenden.
        ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor);

        //Das Passende Ausgabeformat festgelegen.
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);
        if (strDate) {
            var date = moment(strDate);
            if (date.isValid()) {
                $(element).val(date.format(dateformatStr));
            }
        }
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);
        if (strDate) {
            var date = moment(strDate);
            if (!date.isValid()) {
                date = moment(strDate, dateformatStr);
            }

            if (date.isValid()) {
                //Hier kann das Datum direkt als ISO Format String abgelegt werden.
                //das passende Property wird dabei anhand der ElementId ermittelt.
                bindingContext.$data[element.id](date.format());
                //Value binding einstellen - wird benötigt, da sonst unsere custom
                //UpdateFunktion nicht aufgerufen wird, wenn wir unseren Wert ändern.
                ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
                //Den Formatierten Wert ausgeben
                $(element).val(date.format(dateformatStr));
                return;
            }
        }

        //Den Standardwert wieder herstellen, der nicht Valide war.
        $(element).val(strDate);
        //Value binding einstellen
        ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
    }
};

Erstellen unseres bindingHandlers für den DataType “Time”. Der Time Handler speichert im knockoutjs Model immer das Aktuelle Datum und die in der Oberfläche angegebene Uhrzeit, sollte also auch das Datum wichtig sein, dann muss später beim Auswerten der Daten auf der Serverseite aufgepasst werden und hier z.B. nur die Uhrzeit extrahiert werden.

//Bindinghandler für die Uhrzeit erstellen.
ko.bindingHandlers.timeValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor);
        var value = valueAccessor(), strTime = ko.utils.unwrapObservable(value);
        if (strTime) {
            var date = moment(strTime);
            if (date.isValid()) {
                $(element).val(date.format(timeformatStr));
            }
        }
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);

        if (strDate) {
            var date = moment(strDate);
            if (!date.isValid()) {
                date = moment(strDate, timeformatStr);
            }

            if (date.isValid()) {
                //Neues "Datum" anlegen - damit hier die Passende Urhzeit gesetzt werden kann
                //denn wir benötigen ein gültiges ISO Datum für einen erfolgreichen Postback.
                var orgDate = moment();
                //die passenden Stunden und Minuten aus der Textbox setzten und dann Serverseitig die
                //passende Uhrzeit zu "extrahieren"
                orgDate.hours(date.hours());
                orgDate.minutes(date.minutes());
                bindingContext.$data[element.id](orgDate.format());
                ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);

                $(element).val(date.format(timeformatStr));
                return;
            }
        }

        $(element).val(strDate);
        //Value binding einstellen
        ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
    }
};

Der “Rest” bleibt wie gewohnt, d.h. ein einfaches Submit was einen WebAPI Aufruf startet und dann unser Model mit den Daten an unsere C# Funktion übergibt.

//Funktion die aufgerufen wird, wenn das Objekt erfolgreich angelegt werden konnte
function CreatedSuccess(data) {
    //Das Model neu "binden" um die Daten auf der Oberfläche zu aktualisieren
    ko.applyBindings(data);
}

//Submit der Formulars und der Daten aus dem Model
$("form").submit(function (event) {
    event.preventDefault();
    var model = JSON.stringify(ko.toJS(mod12));

    $.ajax({
        //url: "/api/personEditApi/SaveWithParams?take=10&skip=20",
        url: "/api/personEditApi/SavePerson",
        //url: "/api/personEditApi/Save",
        type: "POST",
        data: model,
        contentType: 'application/json; charset=utf-8',
        statusCode: {
            200 /* OK */: function (data) {
                CreatedSuccess(data);
            },
            201 /* Created */: function (data) {
                CreatedSuccess(data);
            },
            //400 /* BadRequest*/: function () {
            //    //Die ModelRevalidation wird automatisch aufgerufen, 
            //    //die Untersuchung auf Bad Request ist nicht notwendig.
            //    //Es muss nur die Klasse "ValidateAttribute" für die 
            //    //WebApi Registriert werden.
            //}
        }
    });
});

4. Quelle und Codeplex Beispiel

Ich bin mir nicht Sicher ob es sich bei meiner Lösung um die Beste Möglichkeit handelt das Problem zu lösen, daher freue ich mich über Kommentare die mir evtl. sagen können ob ich richtig liege oder mir Verbesserungsvorschläge liefern können.

Quelle:

http://denverdeveloper.wordpress.com/2012/10/02/knockout-helpful-date-time-tips/

Codeplex:

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs” und “Controllsers/PersonEditApiController.cs”

MVC4 WebAPI und knockoutjs – Teil 2 – Validation


Auch wenn man die WebApi benutzt und AJAX Requests ausführt ist es problemlos möglich die Modelvalidierung von MVC zu benutzen. Dafür müssen wir den View wie bisher erstellen und z.B. auf Attribute wie “Required” in unserem Model zurückgreifen.

Im View muss weiterhin “ValidationMessageFor” für das jeweilige Eingabefeld verwendet werden.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Vorname, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Vorname, new { data_bind = "value: Vorname" }) @Html.ValidationMessageFor(model => model.Vorname)</div>
</div>
<pre>

Um die native Model Validierung von MVC auszuführen/anzuzeigen, gibt es zwei Möglichkeiten.

1. Validierung für alle Requests automatisch einstellen.

Für diese Variante muss ein FilterAttribut erstellt werden und global für alle WebApi Aufrufe registriert werden.

 /// 
 /// ValidationAttribute für WebApi Zugriffe die nicht valide sind.
 /// 
 public class ValidateAttribute : ActionFilterAttribute
 {
     public override void OnActionExecuting(HttpActionContext actionContext)
     {
         if (!actionContext.ModelState.IsValid)
         {
             actionContext.Response = actionContext.Request.CreateErrorResponse(
                 HttpStatusCode.BadRequest,
                 actionContext.ModelState);
         }
     }
 } 

Registrieren des ValidateAttribute, wird bei mir vor dem Registrieren der Routen gemacht

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Registrieren unseres Filters für die Modelvalidation für AJAX Requests
        config.Filters.Add(new ValidateAttribute());

        ......

Wenn jetzt ein AJAX Request mit mit einem “Aktuellen” Model stattfindet z.b. mit “Required” Attribute werden die Fehlermeldungen im Webformular angezeigt und es werden keine Daten am Controller auf dem Server “ankommen”. Es wird vom Server ein StatusCode 400 zurückgegeben, der bei Bedarf auch noch ausgewertet werden kann.

2. Validierung nur für bestimmte Controllerfunktionen durchführen.

Hier wird kein ValidateAttribute angelegt, sondern direkt in der jeweiligen Controllerfunktion eine HttpResponseException ausgelöst, wenn das Model nicht valide ist. Anhand dieser Exception, weiß der Client auch das er einen Fehler anzeigen soll.

public PersonModel Save(PersonModel value)
{
    if (ModelState.IsValid)
    {
        if (value == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        //Gibt den Statuscode 200 für OK zurück!
        value.Id = 5;
        return value;
    }

    throw new HttpResponseException(HttpStatusCode.BadRequest);
}

Codeplex:

https://squadwuschel.codeplex.com/

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs”, “Controllsers/PersonEditApiController.cs” und “Helpers/Filters/ValidateAttribute.cs”

MVC4 WebAPI und knockoutjs – Teil 1 – Modelbinding


Um dynamische und flexible Oberflächen zu erstellen, lohnt es sich einen Blick auf die WebAPI in Verbindung mit knockoutjs zu werfen. Die Webseite von knockoutjs bietet hier bereits einen sehr guten Überblick und eine gute Einführung wie genau knockoutjs funktioniert, daher werde ich darauf nicht im Detail eingehen.

Mit Hilfe von MVC4, WebAPI und knockoutjs lassen sich MVC Oberflächen sehr einfach aktualisieren. Ich versuche es hier mit dem normalen knockoutjs + mapping Plugin, damit ist es möglich direkt das MVC Model in ein knockoutjs Model umzuwandeln.

Das folgende Beispiel zeigt, wie man ein Model erstellt und dann z.B. beim Speichern keinen Postback auslöst, sondern das ganze per Ajax Call abspeichern kann und die Oberfläche aktualisiert.

1. Erstellen eines passenden Models in C#, welches dann in ein JSON Objekt umgewandelt wird.

public class PersonModel
{
        [Display(Name = "ID")]
        public int Id { get; set; }

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

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

2. Wir nutzen einen normalen Controller und einen normalen View um unsere Daten in der Bearbeiten Ansicht darzustellen.

Wir passen den View aber bereits an unser knockoutjs Model an, d.h. wir Nutzen TextBoxFor, da wir hier zusätzliche HTML Attribute erstellen können. Wir erstellen hier “data_bind” welches dann im HTML als “data-bind” gerendert wird und setzten den passenden Value der aus unserem Model per knockoutjs angezeigt bzw. aktualisiert werden soll.

<div class="control-group">
     @Html.LabelFor(model => model.Vorname, new { @class = "control-label" })
     <div class="controls">
         @Html.TextBoxFor(model => model.Vorname, new { data_bind = "value: Vorname" })
         @Html.ValidationMessageFor(model => model.Vorname)
     </div>
</div>

3. Anlegen unseres API Controllers.

Hier gibt es zwei Funktionen, die beide das “gleiche” machen. Die Daten die wir mit dem Postback erhalten, werden in unser PersonModel “umgewandelt” und wir können können die Daten jetzt Speichern, …. Wir passen aber nur die ID an und geben unser Aktualisiertes Model wieder zurück, damit wir auch auf der Webseite sehen, das sich die ID geändert hat.

public class PersonEditApiController : ApiController
{
    /// <summary>
    /// POST api/PersonEditApi/SavePerson
    /// </summary>
    public HttpResponseMessage SavePerson(PersonModel value)
    {
        if (ModelState.IsValid)
        {
            if (value == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            value.Id = 5;
            //Wird automatisch in JSON Objekt umgewandelt und gibt den StatusCode 201 für Created zurück!
            HttpResponseMessage response = Request.CreateResponse<PersonModel>(HttpStatusCode.Created, value);
            response.Headers.Location = new Uri(Request.RequestUri, "/api/PersonEditApi/SavePerson/" + value.Id.ToString());
            return response;     
        }

        throw  new HttpResponseException(HttpStatusCode.BadRequest);
    }

    /// <summary>
    /// POST api/PersonEditApi/Save
    /// </summary>
    public PersonModel Save(PersonModel value)
    {
        if (ModelState.IsValid)
        {
            if (value == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            //Gibt den Statuscode 200 für OK zurück!
            value.Id = 5;
            return value;
        }

        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }
}

4. Hinzufügen einer WebApi Route “AlternateWebApi”

Damit wir den PersonEditApiController auch so nutzen können wie wir es bisher von den MVC Controllern gewohnt sind, müssen wir noch eine weitere Route zu unserer WebApi hinzufügen. Denn mit der Standardroute für API aufrufe sind nur GET/POST/DELETE Abfragen möglich. Aber meist hat man mehr wie nur diese “paar” Abfragen die man für einen View benötigt, daher fügen wir noch eine neue Route vor der “DefaultApi” Route hinzu.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Alternatives Routing, hier ist es jetzt wieder möglich gezielte Actionen und Controller abzufragen
        //In der DefaultApi kann man nur Get/Post/Delete abfragen und ist damit sehr begrenzt was die Abfragen angeht.
        config.Routes.MapHttpRoute(
            name: "AlternateWebApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        //Standardroute  - hier lässt sich nur GET/POST/DELETE abfragen.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }

5. Anpassen unseres JavaScript Codes im View ‘cshtml’, wo die Daten angezeigt werden sollen.

Das Aktuelle C# Model in ein JSON Model Serealisieren und mit dem knockoutjs Modelmapping direkt ein Model erstellen lassen und dieses an unser Formular binden.

Wenn der User den Submit Button klickt, wird unser knockoutjs Model wieder in JSON umgewandelt und vom JSON Format dann in einen String umgewandelt, welcher per API Aufruf übergeben wird. Aktuell werden hier zwei StatusCodes ausgewertet, da wir den Statuscode 200 bei der “Save” Abfrage zurückbekommen und die 201 bei “SavePerson”. Beide Aufrufe führen aber zum gleichen Ergebnis.

(Achtung es gibt auch eine ko.toJSON() Funktion, diese darf hier NICHT verwendet werden, da sonst ein Model erstellt wird welches nicht Valide mit unserem C# Objekt ist und unsere Funktion in der C# API als Model “null” übergeben bekommt.)

var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

function CreatedSuccess(data) {
    ko.applyBindings(data);
}

$("form").submit(function (event) {
    event.preventDefault();
    var model = JSON.stringify(ko.toJS(mod12));

    $.ajax({
        url: "/api/personEditApi/Save",
        type: "POST",
        data: model,
        contentType: 'application/json; charset=utf-8',
        statusCode: {
            200 /* OK */: function (data) {
                CreatedSuccess(data);
            },
            201 /* Created */: function (data) {
                CreatedSuccess(data);
            }
        }
    });
});

 

Aktuell habe ich noch Probleme beim anzeigen und Konvertieren von Datumsangaben beim Modelbinding, daher verwende ich auch “JsonConvert” und nicht die Standard Json.Serialize Funktion von MS, denn dann werden nur Ticks angezeigt und mit JsonConvert wird das ISO Format für das Datum verwendet. Wenn ich diese Problematik in den Griff bekommen habe, wird es ebenfalls einen eigenen Post geben. Bzw. wenn jemand bereits Erfahrungen damit gesammelt hat freue ich mich gern über einen Kommentar :-).

Codeplex:

https://squadwuschel.codeplex.com/

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs” und “Controllsers/PersonEditApiController.cs”

Quellen:

http://www.west-wind.com/weblog/posts/2012/Aug/21/An-Introduction-to-ASPNET-Web-API

http://www.asp.net/web-api/overview

http://code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7/sourcecode?fileId=63598&pathId=212749278

http://knockoutjs.com/index.html