How to: Minify require.js Scripts in ASP.net

Ohne Frage, das Bundling in ASP.net ist eine feine Sache – so lange Scripts zusammengefasst werden können, die immer benötigt werden und nicht dynamisch per require.js bei Bedarf nachgeladen werden.

Für diese Scripts gibt es 2 Möglichkeiten.

  1. Die Scripts werden entweder direkt durch einen Script-Minifier gejagt und ausgeliefert – dafür gibt es diverse Tools, die entweder auf die Änderung eines Scripts reagieren oder bei einem Build ausgeführt werden
  2. Die Scripts werden zur Laufzeit optimiert

Ich möchte hier auf die 2. Möglichkeit eingehen und zeige, wie Scripts zur Laufzeit – unter Verwendung von WebGrease – minimiert werden können.

WebGrease installieren

WebGrease kann über nuget eingebunden werden: Install-Package WebGrease

Der Minifier

Um die Ausgabe an den Client verändern zu können, muss der „Filter“ der Response auf ein Objekt gesetzt werden, dass die Arbeit anstelle des Default-Filters übernimmt.

Response.Filter:
Summary: 	Gets or sets a wrapping filter object that is used to modify the 
		HTTP entity body before transmission.
Returns:	The System.IO.Stream object that acts as the output filter.

Konkret bedeutete das, dass ein Objekt benötigt wird, das von Stream ableitet, um es dem Filter zuzuweisen. Diesem Objekt wird der Output-Stream, auf den das minimierte Script geschrieben werden soll:

public class JavascriptRequestMinifier : MemoryStream {
	private readonly Stream _stream;
	
	///<summary>Erzeugt eine Instanz des Objekts</summary>
	///<param name="stream">Der Output-Stream</param>
	public JavascriptRequestMinifier(Stream stream) {
		this._stream = stream;
	}
	//TODO: Write + Flush implementieren
}

Als nächstes muss die Methode ‚Write‘ überschrieben werden, um das Schreiben auf den Output-Stream abzufangen.
Das Framework beschreibt den Stream blockweise, was anfangs dazu führte, dass bei größeren Scripts nicht das Ganze optimiert und ausgegeben wurde, sondern nur ein Teil davon. Abhilfe schafft ein StringBuilder, um den Output zu sammeln und ihn komplett in die Ausgabe zu schreiben, wenn der Stream geschlossen wird: 

readonly StringBuilder _codeBuilder = new StringBuilder();

public override void Write(byte[] buffer, int offset, int count) {
	// string aus buffer extrahieren und _codeBuilder zuweisen, falls das script 
	// länger ist
	_codeBuilder.Append(Encoding.UTF8.GetString(buffer));
}

Abschließend muss die Flush-Methode überschrieben werden, um die gesammelten Daten minimiert an den Client zu senden:

public override void Flush() {
	Minifier m = new Minifier();
	string code = m.MinifyJavaScript(
		this._codeBuilder.ToString(), 
		new CodeSettings {
			PreserveImportantComments = false,
			PreserveFunctionNames = true,
			MinifyCode = true
		}
	);
	this._stream.Write(
		Encoding.UTF8.GetBytes(code), 0, Encoding.UTF8.GetByteCount(code));
	this._stream.Flush();
}

Den Filter setzen

Der letzte Schritt, um das Script minimiert auszugeben ist, den Filter zuzuweisen. Die richtige Stelle ist hier in der Global.asax der Event ‚PostReleaseRequestState‘

// Occurs when ASP.NET has completed executing all request event handlers and the
// request state data has been stored.

An dieser Stelle muss geprüft werden, ob es sich bei dem ContentType um ein Javascript handelt, dass optimiert werden soll.

Ich habe require.js dahingehend konfiguriert, dass dem Script ein QueryString-Parameter ‚min-require=1‘ angefügt wird. Enthält die Query der Url des Request diesen String, wird eine Instanz des Minifiers erzeugt und dem den Response.Filter zugewiesen:

protected void Application_PostReleaseRequestState(Object sender, EventArgs e){
	if (this.Request.Url.Query.Contains("min-require=1")) {
		this.Response.Filter = 
			new JavascriptRequestMinifier(this.Response.Filter);
	}
}

Damit werden alle Scripts, die dynamisch geladen werden, minimiert an den Client übertragen.

Advertisements

Javascript-Minifier beim Response – ohne Bundles

ASP.net bringt von Haus aus ja bekanntlich die Funktionalität mit, viele Javascripts zu einem Bundle zusammenzufassen und diese minimiert an den Client zu übertragen.

Was jedoch, wenn man Scripts hat, die dynamisch – z.B. mit require.js nach – nachgeladen werden?

Die eine Möglichkeit ist, (1)diese nach der Erstellung direkt zu minimieren – eine andere wäre, (2)das Script dynamisch zur Laufzeit zu optimieren und an den Client zu geben.

Zweiteres möchte ich in diesem Post näher Beleuchten.

Um Javascript zur Laufzeit zu minimieren, bedarf es einem Eingriff in der global.asax. Hier kann man sich den PostReleaseRequestState-Event zunutze machen.

Die Beschreibung zu diesem Event lautet:

Occurs when ASP.NET has completed executing all request event handlers and the request state data has been stored.

Und einer Helfer-Klasse, die das minimieren des Scripts übernimmt. Diese nimmt den OutputStream entgegen und minimiert mit Hilfe des Minifier-Objekts aus dem Namespace Microsoft.Ajax.Utilities die Ausgabe:

public class JavascriptRequestMinifier : MemoryStream{
private readonly Stream _stream;

public JavascriptRequestMinifier(Stream stream){
this._stream = stream;
}

public override void Write(byte[] buffer, int offset, int count){
string code = Encoding.UTF8.GetString(buffer);
Minifier m = new Minifier();
code = m.MinifyJavaScript(code, new CodeSettings {
EvalTreatment = EvalTreatment.MakeImmediateSafe,
PreserveImportantComments = false
});

this._stream.Write(
Encoding.UTF8.GetBytes(code),
offset,
Encoding.UTF8.GetByteCount(code));
}
}

In der Global.asax wird nun noch der PostReleaseRequestState-Event behandelt und geprüft, ob es sich um ein Javascript handelt – ist das der Fall, tritt der JavascriptRequestMinifier in Aktion:

protected void Application_PostReleaseRequestState(
Object sender, EventArgs e) {

if (this.Response.ContentType.Equals(„application/javascript“)) {
this.Response.Filter = new
JavascriptRequestMinifier(this.Response.Filter);
}
}

ACHTUNG: An der Stelle wird der Minifier immer dann verwendet, wenn es sich um eine Response handelt, dessen ContentType „application/javascript“ ist.

Das heißt, auch bereits minimierte Scripts werden noch einmal versucht, zu minimieren – was natürlich gelegentlich auch klappt, und das ein oder andere Byte zusätzlich gespart werden kann!

Um das zu verhindern, könnte man ggfs. prüfen, ob der angefragte Dateiname auf .min.js endet oder zu minimierende Scripts immer mit QueryString-Argumenten aufrufen und diese dann zu prüfen…

 

Komplexe Objekte & Arrays mit jquery $.post an einen ASP.net MVC-Controller senden

Das Szenario

Ich wollte ein komplexes Javascript-Objekt auf dem Client erzeugen und per $.post an den Server schicken. Ein einfaches Aufbauen des Objekts hat so lange funktioniert, bis ich ein Array mit weiteren Objekten erzeugt habe.

So bin ich gestartet

Mein Model auf der Serverseite sieht ungefähr so aus:

public class DataModel {
    public string Id { get; set; }
    public List<Data> Items { get; set; }
}
public class Data{
    public string[] Key { get; set; }
    public int Value { get; set; }
}

Auf dem Client habe ich dafür folgendes Aufgebaut

var data = {
    id: 'foo',
    Items: [{ Key: [ 'a', 'b' ], Value: 666 },
            { Key: [ 'c', 'd' ], Value: 999 }]
}

Nachdem ich das losgeschickt hatte, war DataModel.Id gesetzt, Items blieb NULL…

Die Lösung

jQuery ist manchmal ein…nicht ganz so nettes Ding! Ich habe viel hin und her probiert, wie ich das Model auf dem Client zusammenbauen muss, damit etwas vernünftiges auf dem Server ankommt. Dabei habe ich mich an die Notation erinnert die notwendig ist, wenn man die Daten per Form-Serialisierung versenden möchte.

Hier erhalten die Felder im name-Attribute den ‚Pfad‘, an den der Wert am Ende landen soll. Konkret heißt das, dass ein Feld, dessen Wert in DataModel.Items[0].Value stehen soll, den „name“ Items[0].Value besitzen muss.Für den Key wäre der „name“ Items[0].Key[0], Items[0].Key[1], …

Und genau SO muss dann auch der Name des Wertes im ViewModel aussehen:

var data = {
    id: 'foo' 
}
// soweit alles beim alten, jetzt das NEUE
data["Items[0].Value"] = 666;
data["Items[0].Key[0]"] = 'a';
data["Items[0].Key[1]"] = 'b';

data["Items[1].Value"] = 999;
data["Items[1].Key[0]"] = 'c';
data["Items[1].Key[1]"] = 'd';

Damit kann ich dann komplexe Daten wegschicken…

 

Wenn jemand einen besseren Weg kennt, bitte Posten! Ansonsten: Viel Spass und Erfolg damit!

 

ASP.net MVC – Kein Zugriff auf Scripts in der Area

Ein Problem, dass jemanden ein wenig beschäftigen kann, wenn mach versucht ein MVC-Projekt zu strukturieren und alle Dinge, die im Kontext eines Bereichs gültig sind, auch dort zu haben.

In meinem Fall habe ich ein bestehendes MVC-Projekt so umstrukturiert, dass Views, PartialView, Controller, Models und eben auch Scripts, in einem Unterordner einer Area stehen.
Versucht man nun das Script z.B. über Url.Content zu laden, erhält man eine HttpNotFound-Exception. Schuld daran sind – bei mir – 2 Einträge in der Web.config, die im Areas-Ordner hinterlegt ist. Diese steuern, dass man von außen keinen Zugriff auf alle Dateien hat, die unterhalb des Pfads „Areas“ liegen:

<system.web>
<httpHandlers>
<add path=“*“ verb=“*“ type=“System.Web.HttpNotFoundHandler“/>   
</httpHandlers>
</system.web>

<system.webServer>
<handlers>
<remove name=“BlockViewHandler“/>
      <add name=“BlockViewHandler“ path=“*“ verb=“*“ preCondition=“integratedMode“ type=“System.Web.HttpNotFoundHandler“/>
</handlers>
</system.webServer>

Ansich eine gute Sache, schließlich möchte man nicht, dass man Views über die physische URL anspricht. Es verhindert allerdings auch das Laden von Scripts aus diesem und allen Unterverzeichnissen.

Die Lösung ist denkbar einfach:

<system.web>
<httpHandlers>
      <add path=“*.aspx“ verb=“*“ type=“System.Web.HttpNotFoundHandler“/>   
      <add path=“*.ascx“ verb=“*“ type=“System.Web.HttpNotFoundHandler“/>   
</httpHandlers>
</system.web>

<system.webServer>
<handlers>
<remove name=“BlockViewHandler“/>
      <add name=“BlockViewHandler“ path=“*.aspx“ verb=“*“ preCondition=“integratedMode“ type=“System.Web.HttpNotFoundHandler“/>
      <add name=“BlockViewHandler“ path=“*.ascx“ verb=“*“ preCondition=“integratedMode“ type=“System.Web.HttpNotFoundHandler“/>
</handlers>
</system.webServer>

Mit diesen Einstellungen ist nun nur noch der Zugriff auf aspx und ascx-Dateien gesperrt. Scripts können problemlos geladen werden.

Hoffentlich konnte ich dem ein oder anderen ähnliche Probleme ersparen!

jQuery UI Slider & Fixed Range

Ein Slider ist mit jQueryUI schnell erstellt. Schwieriger wird es erst, wenn man ihn modifizieren möchte.

Mein Problem bestand darin, einen Slider zu bauen, bei dem ein Bereich als optimal hervorgehoben werden sollte.

Ich hab mich dafür entschieden, eine Textbox mit data-Attributen zu versehen um diese dann zur Laufzeit per Javascript zu einem Slider mutieren zu lassen. Dert Wert der Textbox wird dabei als Slider-Starteinstellung genutzt.

Attribute
Name Verwendung Beispiel
data-slider Kommasepariert *1) minimaler und maximaler Wert für den Slider data-slider=“0,10″
data-sliderFixedRange Kommaseparierte *1) Werte für den hervorzuhebenden Bereich data-sliderFixedRange=“5,7″

*1: Das Trennzeichen ist über das jQuery-Plugin $dgSlider_Settings.defaults.valueSplitSign einstellbar.

Die Textbox sollte dann so aussehen:

<input type=“text“ id=“myId“ name=“myName“ data-slider=“0,10″ data-sliderFixedRange=“5,7″ />

Um aus dem Element nun einen Slider zu machen muss einfach $(‚:text[data-slider]‘).dgSlider(); aufgerufen werden.

Abschließend noch das Script:

(function($) {
$.dgSlider_Settings = {
_dataKey: ‚dgSliderDataKey‘,
defaults: {
textbox: null,
slider: null,
valueSplitSign: ‚,‘
} /* end defaults */
}

var methods = {
_init: function(settings) {
$(this).each(function() {
var $this = $(this);
var data = methods._getData($this, settings);

data.textbox = $this;
data.slider = $(„<div />“);
data.slider.insertBefore(data.textbox);
var opt = methods._createOptions(data.textbox);
data.textbox.hide();
data.slider.slider(opt);
});
},
destroy: function() {
$(this).each(function(){
var data = methods._getData($(this));
data.slider.slider(„destroy“);
data.slider.remove();
data.textbox.show();
});
},
/* Erzeugt die Options für den Slider aus den Attributen der Textbox */
_createOptions: function(textbox) {
var data = methods._getData($(this));
var sliderValues = textbox.attr(„data-slider“).split(data.valueSplitSign);
var opt = {
min: parseInt(sliderValues[0]),
max: parseInt(sliderValues[1]),
step: 1,
value: textbox.val()
};

if (textbox.attr(„data-sliderFixedRange“)) {
var fixedRange = textbox.attr(„data-sliderFixedRange“).split(data.valueSplitSign);

var startPoint = (parseInt(fixedRange[0]) * 100) / (opt.max – opt.min);
var rangeWidth = (((parseInt(fixedRange[1]) * 100) / (opt.max – opt.min)) – startPoint);

opt.create = function(evt, ui) {
var rangeDiv = $(„<div/>“).css({
position: ‚relative‘,
left: startPoint + „%“,
width: rangeWidth + „%“,
height: „100%“
}).addClass(‚bg90‘);
$(this).append(rangeDiv);
}
}
opt.change = function(evt, ui) { textbox.val(ui.value); }
return opt;
},
/* Liefert das .data-Objekt */
_getData: function(x, settings) {
var data = x.data($.dgSlider_Settings._dataKey);

if (!data) {
data = $.extend({}, $.dgSlider_Settings.defaults, settings);
x.data($.dgSlider_Settings._dataKey, data);
}
return data;
}

}

$.fn.dgSlider = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === ‚object‘ || !method) {
return methods[„_init“].apply(this, arguments);
} else {
jQuery.error(„Die Methode “ + method + “ konnte nicht gefunden werden“);
}
}

})(jQuery);

ASP.net SignalR

Live, Live, Live… Etwas, was viele von uns im Web vermissen, ist die Möglichkeit, die Kommunikation vom Server zum Client zu ermöglichen. Zum Beispiel dann, wenn ich die Besucher meiner Webseite oder die User innerhalb einer WebApplication immer aktuell mit Informationen versorgen will, ohne Ihnen zuzumuten einen Refresh der Seite zu machen.

Es gibt eine nicht allzu große Anzahl von möglichen Lösungen zu finden, diese unterscheiden sich aber – natürlich – von Browser zu Browser.

Eine wunderbare und vor allem einfach zu implementierende Lösung – auf die ich schon vor einiger Zeit aufmerksam wurde – ist die Komponente SignalR – ein „Freizeitprojekt“ zweier Entwickler aus dem Hause Microsoft.

Auf der Client-Seite brauche ich wenige Zeilen Javascript, auf dem Server, für eine einfache Chat-Funktion, ebenfallt nur einige Zeilen.

Ich will das Ganze kurz im Umfeld einer ASP.net MVC4-Anwendung aufzeigen und ein kleines Chatprogramm schreiben.

Nachdem die Anwendung angelegt wurde, sollten die Referenzen für hinzugefügt werden. Ich mache das über NuGet…

Die Schnittstelle zwischen Server und Client stellt eine Klasse bereit, die von SignalR.Hubs.Hub ableitet. Diese Klasse nenne ich MessageHub

Weiter implementiere ich die Interfaces IConnected und IDisconnect um auf Connect-, Disconnect- und Reconnect-Ereignisse reagieren zu können.

Zusätlich stelle ich 2 Funktionen bereit:

  • JoinGroup: Fügt den Client zu einer Gruppe hinzu
  • SendMessage: Sendet eine Nachricht an alle verbundenen Clients innerhalb der Gruppe

Das Ergebnis sieht dann so aus:

public class MessageHub : Hub, IConnected, IDisconnect
{
// Liste aller verbundenen Clients und der zugeordneten Gruppen
private static Dictionary<string, string> _GroupMembers = new Dictionary<string, string>();

/// <summary>
/// Methode, um einer Gruppe beizutreten
/// </summary>
/// <param name=“group“>Name der Gruppe</param>
public void JoinGroup(string group) {
Groups.Add(Context.ConnectionId, group);
_GroupMembers[Context.ConnectionId] = group;
}

/// <summary>
/// Sendet eine Nachricht innerhalb der Gruppe, in der der Client
/// registriert ist
/// </summary>
/// <param name=“message“>Nachricht, die verteilt werden soll</param>
public void SendMessage(string message)
{
Clients[_GroupMembers[Context.ConnectionId]].receive(message);
}

/// <summary>
/// Wird aufgerufen, wenn der Client getrennt wird
/// </summary>
/// <returns></returns>
public System.Threading.Tasks.Task Disconnect()
{
_GroupMembers.Remove(Context.ConnectionId);
return Clients.leave(Context.ConnectionId);
}

/// <summary>
/// Wird aufgerufen, wenn ein Client verbunden wurde
/// </summary>
/// <returns></returns>
public System.Threading.Tasks.Task Connect()
{
return Clients.joined(Context.ConnectionId, DateTime.Now.ToString());
}

/// <summary>
/// Wird aufgerufen, wenn ein Client erneut verbunden wird
/// </summary>
/// <param name=“groups“></param>
/// <returns></returns>
public System.Threading.Tasks.Task Reconnect(IEnumerable<string> groups)
{
return Clients.rejoined(Context.ConnectionId, DateTime.Now.ToString());
}
}

Auf der Clientseite wird es dann noch viel einfacher. Das SignalR-Framework erzeugt Javascript-Code, der über die URL „~/signalr/hubs“ eingebunden werden muss. Zusätzlich braucht man das JS-Framework, das über den Pfad „~/Scripts/jquery.signalR-0.5.3.js“ verfügbar ist.

Hier kann dann auf die Methoden (joinGroup und sendMessage – ACHTUNG, die Methoden beginnen mit kleinen Buchstaben) zugegriffen werden.

Über jQuery ist SignalR über $.connection erreichbar. Der erstellte Hub fügt sich nahtlos mit seinem Namen in diesen Kontext ein: $.connection.messageHub.

$(function () {
var messageHub = $.connection.messageHub;
messageHub.name = „room1“;
/* Wird aufgerufen, wenn eine Nachricht vom Server gesendet wird */
messageHub.receive = function (message) {
$(„#messages“).append(„<br />“ + message);
}

$(„#send-button“).click(function () {

/* sendet eine Nachricht an den Server*/
messageHub.sendMessage($(„#text-input“).val());
});
/* starten der Connection */
$.connection.hub.start({
/* Callback, wenn die Verbindung aufgebaut wurde */
callback: function (){
/* Einer Gruppe beitreten */
messageHub.joinGroup(messageHub.name);
}
});
}); /* end $(function) */

Und das war es im Prinzip…

Docking Panels mit Javascript

Die Entwicklung einer Applikation für das Web stellt einen doch immer und immer wieder vor die Aufgabe eine Oberfläche zu schaffen, die vom Look and Feel her wie eine Desktop-Anwendung daherkommt.

Da wäre die immer sichtbare Toolbar oben, eine Statuszeile am unteren Rand des Browserfenster – und natürlich sollen beide beim scrollen nicht verschwinden und auch keinen Content verdecken… Das alles wäre ja noch ganz einfach mit fix positionierten DIV-Container zu realisieren, aber was, wenn die Oberfläche aussehen soll wie z.B. ein Email-Client? Oben eine Toolbar, unten eine Statuszeile, dazwischen dann links eine Navigation die 300px der Bildschirmbreite einnehemen soll, der Rest soll sich dann in einen Bereich zum auswählen von Daten, und der Detailansicht aufteilen – und natürlich den ganzen Platz des Bildschirms ausnutzen.

Wenn man sowas in VisualStudio und WinForms realisert gibt es die Möglichkeit Objekte anzudocken. Ein, zwei klicks, fertig… In HTML und JS ist das eher schwierig – es muss aber möglich gemacht werden.

Die Anforderung an meine DockPanels:

  • Ich bin nicht an eine Anzahl Panels gebunden
  • Die Größe muss sich bei einem Resize des Fensters diesem anpassen
  • Ich muss fixe und variable Höhen und Breiten definieren können

Weiterlesen