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.

IIS Express und Visual Studio – Externen Zugriff erlauben

hierGelegentlich kann es nützlich sein, einen Kollegen auf seinen lokalen IIS-Express zu lassen, während die WebApp im Debug läuft – oder man will sich einfach mal mit einem mobilen Device anschauen, ob das ganze Projekt auch noch auf einem kleinen Display gut aussieht und mit Touch auch wirklich so läuft, wie man sich das gedacht hat…

Über die Einstellungen in Visual Studio kann man das nicht konfigurieren und auch in der UI des IIS-Express wurde ich nicht fündig.

Nachdem ich Bing bemüht habe, klappt es dann doch relativ simpel!

Ich setze voraus, dass die WebApplication bereits über den IISExpress genutzt wird (Project-Settings -> Web -> „Use Local IIS Web Server“). Die Project Url in meinem Fall lautet „http://localhost:7000/digoso-web&#8220;

Zunächst einmal muss die Kommandozeile mit Admin-Rechten gestartet werden, um Konfigurationen über die NetworkShell auszuführen:

netsh http add urlacl url=http://*:7000/ user=Everyone

Damit ebnen wir den Weg für den Zugriff von extern auf unseren IIS (Info zu add urlacl)

Im nächsten Schritt müssen wir manuell in die Konfigurationsdatein ‚applicationhost.config‘ eingreifen. Das Config-File findet sich in aller Regel im Verzeichnis

%HomeDrive%\Documents\IISExpress\config

Im Node „sites“ sollte ein Site-Child liegen, dem als Name -Attribut der Name des Projekts zugewiesen ist. Dieser Node muss editiert werden!

Unter dem Bindings-Node findet man die Information, wie auf die URL zugegriffen werden darf.

<binding protocol=http bindingInformation=*:7000:localhost />

Die Standard-Einstellung stellt sicher, dass nur von localhost der Aufruf erlaubt ist.

Ersetzt man nun ‚localhost‘ gegen den Namen des Computers (z.B. digoso-pc)

<binding protocol=http bindingInformation=*:7000:digoso-pc />

kann man auf den IIS via „http://digoso-pc:7000/digoso-web&#8220; zugreifen und man muss (leider) die Start-Url des IIS so umbiegen, dass sie den Namen des Computers enthält – was allerdings unschön ist, wenn man im Team arbeitet und jeder Computer anders heißt…

Trägt man statt dem PC-Namen einfach ein * ein, kann jeder im Netzwerk auf den IIS mit der URL zugreifen und es müssen keine Änderungen an der Startseite gemacht werden.

<site name="digoso.WebApplication-Site" id="1">
    <application 
        path="/" 
        applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory 
            path="/" 
            physicalPath="\\...\My Web Sites\digoso.WebApplication-Site1" />
    </application>
    <application 
        path="/digoso-web" applicationPool="Clr4IntegratedAppPool">
    <virtualDirectory path="/" 
        physicalPath="......\digoso.WebApplication" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:7000:*" />
    </bindings>
</site>

 

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!

ASP.net MVC – ActionResult mundgerecht geliefert / ViewResult to JsonResult

Problemstellung:

Eine Action auf einem MVC-Controller soll je nach Art des Request entweder ein ViewResult oder ein JsonResult zurückliefern. Dabei soll im Accepts-Header geprüft werden, ob ‚application/json‘ explizit angefordert wurde.

Per Default liefert die Action ein ViewResult zurück, bei einem Json-Request soll das Result-Objekt in ein JsonResult-Objekt umgewandelt und das Model aus ViewResult genutzt werden.

Lösung:

Die richtige Stelle, um die Anfrage zu überprüfen, ist der Einstiegspunkt ‚OnActionExecuted‘. Diese kann – und wird für dieses Beispiel – im Controller direkt überschrieben werden.

Natürlich kann man das Ganze auch in ein ActionFilterAttribute auslagern und hier OnActionExecuted überschreiben.

Zunächst wird überprüft, ob der Client im Request angegeben hat, dass er Json erwartet. Diese Information findet man hier:

filterContext.RequestContext.HttpContext.Request.AcceptTypes -> string[]

Für meine Anforderung hat es gereicht, um zu überprüfen, ob der erste Eintrag ‚application/json‘ entspricht und ob das Ergebnis ein ViewResult ist. Ist das der Fall, wird ein neues JsonResult-Objekt erzeugt und das Data-Property auf das Model aus dem ViewResult gesetzt.

Abschließend wird dem filterContext als Result das neu erzeugte Objekt zugewiesen.

string[] acceptTypes = filterContext.RequestContext.HttpContext.Request.AcceptTypes;
System.Web.Mvc.ViewResultBase vr = filterContext.Result as System.Web.Mvc.ViewResultBase;
if (acceptTypes.Length > 0 
    && acceptTypes[0].Equals("application/json") 
    && vr != null){
        filterContext.Result = newJsonResult() { Data = vr.Model };
}

Um es zu testen, habe ich einen Link erstellt, bei dem der click-Event abgefangen wird:

$("a[data-getJson]").live("click", function (evt) {
    $.ajax({  type: "POST",
             url: this.href,
             headers: { 
                 Accept:  "application/json", 
},
              complete: function (data) {
                        alert(data.responseText);
                    }
                });
                evt.preventDefault();
            });

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);