Ausgeknockt mit Knockout MVC (jetzt auch mit VB.NET)

Knockout.js ist in aller Munde, oder? Und warum auch nicht? Es schafft elegante Abkürzungen, wo wir vorher noch erhebliche Umwege gehen mussten. Kurzum: Es hat sich mal jemand Gedanken gemacht und das Ergebnis hat uns alle aus den Latschen gehauen. Vielleicht auch daher der Name.

In diesem Artikel geht es aber nicht um Knockout.js - daher gehe ich auch nicht auf die Funktionsweise ein – sondern um Knockout MVC. Genauer: Darum, dass letzteres zwar beworben wird mit “Simple C#/VB.NET expressions defining logic of the client side behavior are automatically converted to JavaScript constructions”, gleichzeitig aber bereits bei der Installation in einem VB.NET basierten ASP.NET MVC Projekt scheitert – und woran das liegt.

Problem #1

Das Quickstart Tutorial erklärt, dass zunächst das Paket via NuGet installiert wird:

Install-Package kMVC

Hier das Ergebnis bei einem ASP.NET MVC Projekt, für das VB.NET gewählt wurde (leicht gekürzt):

PM> Install-Package kMVC

"kMVC 0.5.4" wurde erfolgreich installiert.

Ausnahme beim Aufrufen von "Item" mit 1 Argument(en):  "Falscher Parameter. (Ausnahme von HRESULT: 0x80070057 (E_INVALIDARG))"
In C:\…\packages\kMVC.0.5.4\tools\install.ps1:3 Zeichen:1
+ $item = $project.ProjectItems.Item("global.asax").ProjectItems.Item("global.asax ..…

PM>

Die mitunter spannende Information sieht man in der Exception nicht. Aus irgendwelchen Gründen ist der Text gekürzt – und ich war’s nicht.

Das Powershell Skript hat mind. zwei Fehler:

  1. Es wird versucht die Global.asax.cs zu bearbeiten
  2. Die Codezeile, die in der Global.asax den KnockoutModelBinder hinzufügen soll, wird mit einem Semikolon abgeschlossen

Lösung #1

Einfach folgende Zeile der Methode Application_Start in der Global.asax.vb hinzufügen:

ModelBinders.Binders.DefaultBinder = New PerpetuumSoft.Knockout.KnockoutModelBinder()

Problem #2

… ist eigentlich gar keins. Denn während das Tutorial die notwendigen Scripte manuell via Script-Tag hinzufügt, geht der bevorzugte Weg in ASP.NET MVC 4 über Bundles.

Lösung #2

Einfach der Methode RegisterBundles in BundleConfig.vb folgende Zeilen hinzufügen:

bundles.Add(New ScriptBundle("~/bundles/knockout").Include( 
"~/Scripts/knockout-{version}.js",
"~/Scripts/knockout.mapping-latest.js",
"~/Scripts/perpetuum.knockout.js"))

Und abschließend – sofern Knockout MVC global eingesetzt werden soll – in der _Layout.vbhtml View das Bundle im Header laden:

<head> 
< meta charset="utf-8" />
< meta name="viewport" content="width=device-width" />
<title>@ViewData("Title")</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/knockout")
< /head>

Problem #3

Und jetzt wird deutlich, was passiert, wenn man nur an C# denkt. Denn all die Vorarbeit bringt erst einmal gar nichts. Probieren wir es mit dem ClickCounter Beispiel von Knockout MVC.

Schritt 1

Folgender Code ist als ClickCounterModel.vb dem Verzeichnis Models hinzuzufügen:

Imports DelegateDecompiler 

Public Class ClickCounterModel

Public Property NumberofClicks As Integer

<Computed>
Public ReadOnly Property HasClickedTooManyTimes As Boolean
Get
Return Me.NumberofClicks > 3
End Get
End Property

Public Sub RegisterClick()
Me.NumberofClicks = Me.NumberofClicks + 1
End Sub

Public Sub ResetClicks()
Me.NumberofClicks = 0
End Sub
End Class

  

Schritt 2

Folgender Code ist als ClickCounterController.vb dem Verzeichnis Controllers hinzuzufügen:  

Imports PerpetuumSoft.Knockout 

Public Class ClickCounterController
Inherits KnockoutController

'
' GET: /ClickCounter

Function Index() As ActionResult
Return View(New ClickCounterModel())
End Function

Function RegisterClick(model As ClickCounterModel) As ActionResult
model.RegisterClick()
Return Json(model)
End Function

Function ResetClicks(model As ClickCounterModel) As ActionResult
model.ResetClicks()
Return Json(model)
End Function

End Class

Schritt 3

Folgender Code ist als Index.vbhtml dem neuen Verzeichnis Views\ClickCounter hinzuzufügen:

@Imports PerpetuumSoft.Knockout 
@ModelType KnockoutMvcDemoClickCounter.ClickCounterModel

@Code
ViewData("Title") = "Index"
Dim ko = Html.CreateKnockoutContext()
End Code

<h2>Index</h2>

<div>You've clicked @ko.Html.Span(Function(m) m.NumberofClicks) times</div>

@ko.Html.Button("Click me", "RegisterClick", "ClickCounter").Disable(Function(m) m.HasClickedTooManyTimes)

<div @ko.Bind.Visible(Function(m) m.HasClickedTooManyTimes)>
That's too many clicks! Please stop before you wear out your fingers.
@ko.Html.Button("Reset clicks", "ResetClicks", "ClickCounter")
< /div>

@ko.Apply(Model)

   

Und nun das Problem: Einfach mal die Anwendung ausführen. Die Zeile @ko.Apply(Model) löst eine NotImplementedException aus:

System.NotSupportedException wurde nicht von Benutzercode behandelt.
  HResult=-2146233067
  Message=Die angegebene Methode wird nicht unterstützt.
  Source=PerpetuumSoft.Knockout
  StackTrace:
       bei PerpetuumSoft.Knockout.KnockoutExpressionConverter.VisitMethodCall(MethodCallExpression m)
       …

Lösung #3

Sofern man mit VB.NET arbeitet, verhält sich eine Condition in der Methode VisitMethodCall in Knockout MVC anders als in C#:

if (typeof(Expression).IsAssignableFrom(m.Method.ReturnType)) 
return VisitMemberAccess(m.Object, m.Method.Name);

In VB.NET resultiert die Condition immer in einem False und damit löst die letzte Zeile der Methode eine NotImplementedException aus:

throw new NotSupportedException();

Um dieses Problem zu lösen mag folgendes zunächst einmal nicht der korrekte Weg sein, aber erst einmal nur zum Beweis reicht es, die Condition auszukommentieren und Knockout MVC neu zu kompilieren.

Damit werden Firstname und Lastname zwar gefüllt, der Computed Parameter Fullname aber rührt sich nicht. Und ein Blick auf den generierten JavaScript Code offenbart, warum das so ist.

Problem #4

<script type="text/javascript"> 
var viewModelJs = {"Firstname":"Christian","Lastname":"Jacob","Fullname":"Christian Jacob"};
var viewModel = ko.mapping.fromJS(viewModelJs);
viewModel.Fullname = ko.computed(function() { try { return ((this.get_Firstname() + ' ') + this.get_Lastname())} catch(e) { return null; } ;}, viewModel);
ko.applyBindings(viewModel);
</script>

Gesehen?

Beim Generieren des Scripts wurde via Reflection das Model analysiert und dabei die Getter der Properties mit dem Präfix get_ aufgelöst. Spannend daran: Auch hier verhalten sich C# und VB.NET anders, denn während der kompilierte IL Code in beiden Sprachen Property-Getter und –Setter tatsächlich mit get_ und set_ präfixt, liefert die Reflection bei C# Code zur Laufzeit kein Präfix zurück. Und da der ansonsten generierte JavaScript Code ebenfalls nicht mit Präfixen arbeitet, ist das auch korrekt. Nur bei VB.NET kommt es zu dem oben beschriebenen Phänomen und damit kann die Funktion ko.computed die Properties nicht mehr auflösen.

Lösung #4

Auch diese Lösung ist mit Vorsicht zu genießen und erhebt weder Anspruch auf Vollständigkeit oder überhaupt Richtigkeit. Sie ist eher als Proof of Theory zu verstehen.

In der Klasse KnockoutJsModelBuilder muss die Methode AddComputedToModel derart angepasst werden, dass die Rückgabe von DecompileExpressionVisitor.Decompile etwaiges Auftreten von get_ Präfixen unterdrückt.

Beispiel:

sb.Append(ExpressionToString(modelType, DecompileExpressionVisitor.Decompile(Expression.Property(Expression.Constant(model), property))).Replace("get_", ""));

Fazit

Nach diesen beiden Anpassungen funktioniert das Beispiel. Dass Knockout MVC eine sehr schöne Ergänzung zu Knockout.js ist, um es nativen .NET Entwicklern zu erleichtern, auch in den Genuss der Entwicklung reaktiver Webanwendungen zu kommen (ohne beim Gedanken an Datenbindung oder dynamische UIs graue Haare zu bekommen) ist keine Neuigkeit.

Sofern sich die Entwickler von Knockout MVC diese Analyse zu Herzen nehmen, beschränkt sich die Verwendungsmöglichkeit demnächst aber vielleicht nicht mehr nur auf C#-Entwickler.

Comments (1) -

Claas
Claas
9/3/2013 1:20:38 PM #

#Like

Comments are closed

Über die Autoren

Christian Jacob ist Leiter des Geschäftsbereiches Softwarearchitektur und -entwicklung und zieht als Trainer im Kontext der .NET Entwicklung sowie ALM-Themen Projekte auf links.

Marcus Jacob fokussiert sich auf die Entwicklung von Office-Addins sowie Windows Phone Apps und gilt bei uns als der Bezwinger von Windows Installer Xml.

Martin Kratsch engagiert sich für das Thema Projektmanagement mit dem Team Foundation Server und bringt mit seinen Java- und iOS-Kenntnissen Farbe in unser ansonsten von .NET geprägtes Team.

Aktuelle Kommentare

Comment RSS