asp.net: hyperlink disabled, falscher Cursor
Möchte man zur Laufzeit dynamisch einen Link disablen, kann dies sehr einfach erledigt werden mittels
myLink.enabled = false;
Leider bleibt der Cursor bestehen, so dass optisch der Eindruck entsteht, das Element sei anklickbar. Bislang haben wir hierfür eine erweiterte CSS Klasse verwendet und die zur Laufzeit auf den Hyperlink angewendet. Sofern bei einem deaktivierten Link nur der Cursor geändert werden soll, geht es simpler:
myLink.Attributes.CssStyle[HtmlTextWriterStyle.Cursor] = "default";
Et voilà. Mission accomplished!
IIS 7.5 Auto-Start mit .net 4.0 und serviceAutoStartProvider
Standardmässig werden Applikation beim ersten eingehenden Request vom IIS initialisiert. Das kann Sinn machen, ist jedoch für den ersten “lucky guy” mühsam, da er bedeutend länger auf einen Response warten muss als sämtliche Nachfolger. Gerade wenn ein ApplicationPool regelmässig recycled wird und umfangreiche gecachede Objekte wegfallen, können sich je nachdem unschöne Situationen ergeben.
Im konkreten Fall haben wir eine Applikation, die via Webservice täglich neue Daten abholt, diese lokal im Cache ablegt und dadurch eigentlich eine ziemlich gute Performance an den Tag legt. Nun wollen wir aber den erstmaligen Cache-Aufbau nicht dem ersten Besucher zumuten, weswegen wir ein neues Feature von .net 4.0 und IIS 7.5 implementieren: den Auto-Start via ServiceAutoStartProvider! Dieser Weg wurde zwar von Scott Guthrie schon gut beschrieben, trotzdem gibt es ein paar Stolpersteine, die es zu beachten gilt.
Wichtig: es muss zwingend der IIS 7.5 eingesetzt werden und Asp.net 4.0! Die Konfiguration kann zudem nicht über den IIS Manager erfolgen, sondern muss manuell im ApplicationHost Configfile (C:\Windows\System32\inetsrv\config\applicationHost.config) erledigt werden!
1. Schritt : Application Pool StartMode ändern
Im entsprechenden ApplicationPool Element muss [startMode="AlwaysRunning"] eingefügt werden.
<applicationPools> <add name="MyApplicationPool" managedRuntimeVersion="v4.0" startMode="AlwaysRunning" /> </applicationPools>
2. Schritt : Website konfigurieren
Bei der entsprechenden Website muss eine Referenz auf den serviceAutoStartProvider gesetzt werden.
<sites> <site name="MyWebsite" id="1"> <application path="/" serviceAutoStartEnabled="true" serviceAutoStartProvider="PreWarmMyCache" /> </site> </sites>
3. Schritt : Provider definieren
Gleich nach dem schliessenden [/sites] Tag kann nun der serviceAutoStartProvider definiert werden. Und hier lauert der erste kleine Stolperstein: der Typ muss zwingend über den vollständigen Namespace definiert werden.
<serviceAutoStartProviders> <add name="PreWarmMyCache" type="MyNamespace.PreWarmCache, MyAssembly" /> </serviceAutoStartProviders>
Damit sind die Änderungen im Configfile abgeschlossen. Nun fehlt natürlich noch eine entsprechende Klasse innerhalb des Projekts, die dann vom IIS nach dem Recycling der App invoked wird. Hierfür implementieren wir in einer leeren Klasse das IProcessHostPreloadClient Interface.
public class PreWarmCache : System.Web.Hosting.IProcessHostPreloadClient
{
public void Preload(string[] parameters)
{
// cache logic
}
}
Wichtig: wenn unsere neue PreWarmCache-Klasse invoked wird, steht KEIN HttpContext-Objekt zur Verfügung! Dies kann aber umgangen werden, indem man statt HttpContext.Current.Cache[cacheKey] via HttpRuntime.Cache[cacheKey] direkt auf den ApplicationCache zugreift und die Objekte so hinzufügt. Innerhalb der Applikation selber kann danach via HttpContext.Current.Cache[cacheKey] auf das gleiche Objekt zugegriffen werden.
Diese Methode hat sich bewährt, um jeweils nachts die App zu recyceln und gleich den CacheBuilder anzustossen. Gleichzeitig möchte ich aber auch auf einen Nachteil hinweisen: Exceptions in unserer neuen Klasse können dazu führen, dass der ApplicationPool sich abschaltet! Fehlermeldungen finden sich in den Systemlogfiles (resp. Applicationlogfiles) des Betriebssystems.
PDFLib mixed mode assembly in .net 4.0
Um die aktuelle PDFLib 8.x (.net Version) in Visual Studio 2010/.net 4.0 Projekten einsetzen zu können, reicht es leider nicht aus, einfach die Assembly zu kopieren und zu referenzieren. Da es sich bei der PDFLib Assembly um eine “mixed mode assembly” handelt, braucht es eine kleine Ergänzung im app.config des Projekts:
<?xml version="1.0"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> </configuration>
XDocument Download mit Umweg über XmlWriter
Eine kleine Premiere war die Umsetzung einer Exportfunktion via XDocument. Einziger Stolperstein war die Umwandlung des XDocuments in einen Byte-Array, um die Daten als Download via Response Object an den Browser zurückzuschicken. Gelöst haben wir es mit einem Umweg über den XmlWriter:
void OutputXml()
{
XDocument doc = new XDocument();
//
//CREATE XDocument here - Code not included in sample
//
//prepare memory stream for output
MemoryStream ms = new MemoryStream();
XmlWriterSettings xws = new XmlWriterSettings();
//indent xml?
xws.Indent = true;
using (XmlWriter xw = XmlWriter.Create(ms, xws))
{
doc.WriteTo(xw);
}
// convert memory stream to byte array
byte[] byteArray = ms.ToArray();
// clear memory stream
ms.Flush();
ms.Close();
// send object to browser
Response.Clear();
Response.AppendHeader("Content-Disposition", "attachment;filename=export.xml");
Response.AppendHeader("Content-Length", byteArray.Length.ToString());
Response.ContentType = "application/octet-stream";
Response.BinaryWrite(byteArray);
}
Recursive Control Tree Iteration in C#
Leider funktioniert insbesondere bei Verwendung von Masterpages oder nested UserControls das Finden von Controls über die FindControl() Methode ziemlich schlecht. Microsoft hat die Methode (un-)absichtlich nicht rekursiv implementiert. Das führt dazu, dass genau definiert werden muss, auf welcher Hierarchiestufe gesucht werden muss. Die folgende Methode soll ein einfaches Beispiel dafür sein, wie man schnell durch die einzelnen Control-Collections iterieren kann. In diesem konkreten Fall werden alle Controls vom Typ RadTextBox auf einer Form zurückgesetzt.
protected void Page_Load(object sender, EventArgs e)
{
ClearTextBox(this);
}
private void ClearTextBox(Control _c)
{
foreach (Control ctrl in _c.Controls)
{
if (ctrl is RadTextBox)
{
((RadTextBox)(ctrl)).Text = String.Empty;
}
else
{
if (ctrl.Controls.Count > 0)
{
ClearTextBox(ctrl);
}
}
}
}
Verlorene Session nach Löschen – (Lost session after deleting)
Mal wieder ein kniffliges Problem innerhalb einer Webapplikation. In einem unserer CMS Module können angemeldete Benutzer Verzeichnisse auf dem Webserver erstellen und Mediendaten hochladen. Die Verwaltung der einzelnen Assets erfolgt in einem Grid. Nach Beendigung kann via Linkbutton auf die public page zurückgesprungen werden. Die entsprechende URL wird als Objekt in der Session abgelegt. Beim Erstellen hat alles funktioniert, aber scheinbar zufällig war die Sessionvariable komplett leer, was beim nachfolgenden Redirect natürlich zu einer Exception führte.
Erste Vermutungen gingen in Richtung Grid, resp. dass beim Delete die Session gecleared wird – umso erstaunter waren wir über den tatsächlichen Grund: beim Löschen einzelner Assets werden durch die Applikation auch die entsprechenden Files und Ordner im Filesystem entfernt. Beim Entfernen eines Ordners wird jedoch die AppDomain jedesmal recycled, was leider auch die entsprechenden Session Objekte killt. Interessanterweise wird der Recylce nur beim Entfernen von Verzeichnissen ausgelöst, nicht aber bei einzelnen Files.
Umgehen kann man das Problem, in dem entweder die Verzeichnisse später gelöscht werden (batch) oder aber die Session von InProc umstellt auf SqlServer..
SkyDrive ist online!
Nun hat auch Microsoft einen Cloudstorage-Service im Angebot. Im Unterschied zu vielen anderen Anbietern ist der Dienst bei Microsoft kostenlos, vorausgesetzt wird lediglich eine Windows Live ID. Dem Anwender stehen satte 25GB zur Verfügung, die er über die (mehr oder weniger) gewohnte Windows Live Oberfläche verwalten kann.
Zu Beginn stehen dem Anwender vier Ordner zur Verfügung, jeweils ein öffentlicher und ein privater File- und Favoritenordner. Der Upload geschieht sehr einfach über ein paar normale Fileupload-Controls, allerdings ist die Dateigrösse limitiert auf 50MB. Für den privaten Gebrauch dürfte dies aber in den meisten Fällen ausreichend sein – zumindest, solange man nicht Videodateien mit seinen Freunden teilen möchte.
Andere Benutzer von Windows Live können über neue Dateien in den öffentlichen Ordnern informiert werden. Schade, dass keine Download-Einladungen an Personen ausserhalb des Live-Networks verschickt werden können, respektive diese die Datei nicht herunterladen können. Das müsste meiner Meinung nach möglich sein, auch wenn ich nachvollziehen kann, dass Microsoft ein Interesse an neuen Mitgliedern hat.
Ich habe den Dienst nicht wirklich ausgiebig getestet, aber der Zugang scheint sehr simpel zu sein. Leider haben sie es beim Upload verpasst, eine Lösung zu integrieren, die den Upload von ganzen Ordnern erlauben würde (à la Facebook & Co.). Auch den versprochenen Upload via Drag & Drop konnte ich nicht finden. Alles in allem aber eine nette Lösung für den Gebrauch zwischendurch. Sicherheitsbewusste Anwender werden sich aber bei einem Dienst wie dem Schweizer Jointventure Wuala mit einem ausgeklügelten Sicherheitsmechanismus bestimmt wohler fühlen.
Download via Response Object in Firefox – Filename abgeschnitten
Ein automatisch initiierter Download über das Response Object hat in Firefox und bei Filenamen mit Leerzeichen Probleme bereitet. Während der Internet Explorer keine Schwierigkeiten macht, speichert Firefox die Datei unter einem falschen Namen (nämlich nur bis zum ersten Leerzeichen) und ohne Extension. Die Lösung liegt im vom Webserver verschickten Content-Disposition Header, so muss lediglich der Filename in Anführungszeichen gesetzt werden. Beispiel:
using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
namespace MyApp.Desktop
{
public partial class FileDownload : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//check if user is authenticated
if (!User.Identity.IsAuthenticated) { Response.Redirect("~/Default.aspx"); }
int i = 0;
if (Request.Params["itemId"] != null)
{
i = Convert.ToInt32(Request.Params["itemId"]);
}
//class to get fileinfo from db
DocumentDB doc = new DocumentDB(i);
string sourceFile = Server.MapPath("~/Uploads/" + doc.Filename);
System.IO.FileInfo file = new System.IO.FileInfo(sourceFile);
Response.Clear();
Response.AddHeader("Content-Disposition", "attachment; filename="" + doc.FilenameOriginal + """);
Response.AddHeader("Content-Length", file.Length.ToString());
Response.ContentType = "application/octet-stream";
Response.WriteFile(file.FullName);
Response.End();
}
}
}