Användarvänliga url:er till bloggen med IIS7

http://www.codeodyssey.se/upload/resource/blog/iis7-request-model.jpg

Vi har pratat mycket om användarvänliga url:er här på bloggen. Ni som har hängt med på ämnet vet att Microsofts ASP.NET-teknik inte är så optimal för att göra användarvänliga länkar.

Scott Guthrie som är utvecklingschef på Microsoft gör sitt bästa för att hålla modet uppe för alla url-designande freaks där ute. Vi har väntat länge på en lösning och nu kommer den snart i form av Longhorn Server och IIS 7.0.

Scott gjorde i måndags en genomgång av nya finesser i webbservern IIS 7.0 och visar även ett diagram hur processmodellen skiljer sig från den tidigare IIS 6.0. Samtidigt klargörs ytterligare en gång att IIS 7 inte kommer att finnas tillgänglig på Windows 2003.

Så det är väl bara att vänta på det nya serveroperativet då och alla de förbättringar som det kommer att ge.

Det som är bra är att man i alla fall får ordentligt med tid på att tänka igenom exakt hur man vill ha sina url:er när möjligheten finns att förbättra dem. Jag har tidigare spånat lite på hur jag vill ha urler:na till denna bloggen i kommentarerna till en tidigare tråd på ämnet.

Som ett exempel har just detta inlägget du läser just nu adressen:

http://codeodyssey.se/blog.aspx_id=270

Det säger ingenting om innehållet av artikeln för en besökare och det är det jag vill förbättra i första hand. Istället tycker jag att detta skulle vara en snyggare och mer tillgänglig adress.

http://codeodyssey.se/blog/Anvandarvanliga-urler-till-bloggen-med-iis7/

Jag har fortfarande inte bestämt mig helt hur jag ska hantera alla specialtecken som inte stödjs i url:formatet. En som har funderat mer på dessa är Scott Hanselman och han bjuder idag på riktigt intressant läsning i sin artikel "Are Blog URLs important". Scotts blogg har rätt sjyssta url:er men dras fortfarande dras med .ASPX-ändelsen.

Det finns ju några genvägar mot snyggare adresser men vi känner att det bästa skulle vara att vänta tills IIS 7.0 kommer och man kan få till adresserna precis som man vill ha dem med en gång.Adresserna bör ju aldrig ändras så att göra några halvmessyer känns inte aktuellt.

Men för er som länkar till oss kan vara lugna. De url:er vi har just nu, kommer fortsätta att fungerar, även när vi lanserar de mer besökarvänliga varianterna. Trasiga länkar är inget kul.

By Jesper Lind

Förhindra DOS-attacker i webbapplikationen

Att det är en vanlig metod att temporärt "ta ner" webbplatser genom Denial Of Service (DOS) är ett välkänt faktum. Det har under åren skett många hot och icensättningar av överbelastningsattacker mot webbplatser. Ibland politiska aktioner men även utpressning mot krav på pengar.

Bland annat har spelsajter i USA pressats på stora pengar av internetbrottslingar.

Polisens hemsida i Sverige har också blivit utsatt för liknande metoder.

Kortfattat går det till så att webbplatsen överbelastas med ett stort antal anrop och genom att rikta sig mot dokument av stor datamängd.

Så vad kan man göra för att skydda sig mot dessa attacker?

Jo dels kan man göra det genom att använda smart hårdvara som kan känna av ifall trafiken härstammar från otillåtliga källor eller följer ett felaktigt mönster.

Men man kan även bygga in skyddet direkt i sin webb-applikation som Omar AL Zabir visar ett exempel på i sin blogg. Hans metod skriven i ASP.NET C# går ut på att besökarnas IP-adresser sparas i Cache-minnet under en stund och håller reda på att inte för många anrop har kommit från samma källa, inom en tidsrymd.

Smart tänkt och ett bra komplement till hårdvaruskydd.

By Jesper Lind

Tips hur man designar urler till ASP.NET

Som ni vet väntar vi ivrigt på att IIS7 ska kunna gå att använda i produktions miljö. Där kommer det finns bättre stöd för att "skriva om" urler. De nuvarande alternativen är inte så vidare bra och saknaden av en standardiserad modell är jobbig.

Scott Guthrie ger oss dock några bra knep. En genomgång av de ISAPI-filter finns tillgängliga för IIS6. Även andra trix, bland annat Request.PathInfo som jag inte använt tidigare. Då kan man göra urler som ser ut så här.

http://www.webstore.com/products.aspx/Posters
http://www.webstore.com/products.aspx/DVDs
http://www.webstore.com/products.aspx/CD

Ändelsen ".aspx" är helt onödig och den tillför absolut ingenting för besökarens upplevelse. Okej vi som är utvecklare får snabbt reda på vilken teknik som körs, men hur väsentligt är det?

Ett snyggare och mer lättillgängligt variant skulle vara som i nästa exempel.

http://www.webstore.com/products/Posters
http://www.webstore.com/products/DVDs
http://www.webstore.com/products/CD

Men för att ange en sådan struktur måste man alltså använda ett ASAPI ISAPI-filter. Och det är inte alla webbhotell som erbjuder. Man kan ju givetvis skapa fysiska mappar och lägga en Default.aspx fil i varje. Men det orkar man inte..

By Jesper Lind

Kombinerad kolumn i dataset och spara i cacheminne

Har en rutin som hämtar ut olika märken för en e-handelsida. Dessa visas i en DropDownList och fylldes den av en SqlDataReader.

Nu ville jag spara dessa data i ett DataSet och lagra det i Cache-minnet för att spara på dyrbara återtrippar till databasen (SQL Server).

Efter märkena i listan skulle det stå hur många produkter som finns under varje märke, inom paranteser. Därför behövde jag göra en ny kolumn och sätta ihop två värden och lägga till egna tecken.

Inlägget "SubString in an ADO.NET DataSet - (DataColumn Expression)" på CodeBetter.com visar hur man gör en nya kolumn med en SubString och som lägger till tre prickar efteråt.

ds.Tables["Customers"].Columns.Add("ShortCompanyName", typeof(string),"Substring(CompanyName, 1, 10) + '...'");

Med hjälp av började jag förstå hur syntaxen för hur DataColumn.Expression fungerar.

Här är själva kodsnutten som kombinerar två värden och skriver det sista inom paranteser.

dataSet.Tables["ChooseBrand"].Columns.Add("BrandWithCount", typeof(string), "Brand + ' (' + CountOfProducts + ')'");

Här är större del av koden som även visar hur man lägger in ett DataSet i Cache.

private void BindBrand()
{

ddlBrand.Items.Clear();

DataSet ds = LoadBrand();

ddlBrand.DataSource = ds;
ddlBrand.DataSource = ds.Tables[0];
ddlBrand.DataTextField = ds.Tables[0].Columns["BrandWithCount"].ColumnName.ToString();

ddlBrand.DataValueField = ds.Tables[0].Columns["Id"].ColumnName.ToString();
ddlBrand.DataBind();

ListItem titleItem = new ListItem((string)this.GetLocalResourceObject("ChooseBrand"), "0");
ddlBrand.Items.Insert(0,titleItem);

}

protected DataSet LoadBrand()
{
Trace.Write("LoadBrand()");
DataSet dataSet = new DataSet();

string strCacheName = "ChooseBrand";
//Cache.Remove(strCacheName);

//Hämta cachad data om det finns
if (Cache[strCacheName] != null)
{
dataSet = (DataSet)(Cache[strCacheName]);
Trace.Write("//Hämta cachad ChooseBrand");
}
else
{

SqlConnection conn = new SqlConnection(strConn);

string strSQL = "SELECT DISTINCT Brand.Id, Brand.Brand,(SELECT Count(Product.Id) AS CountOfProductId FROM Product WHERE Product.BrandId=Brand.Id) AS CountOfProducts FROM Brand ORDER BY Brand.Brand";

try
{
SqlDataAdapter dataAdapter = new SqlDataAdapter(strSQL, conn);
dataAdapter.Fill(dataSet, "ChooseBrand");
dataAdapter.Dispose();
dataAdapter = null;


//Lägg till custom kolumn
dataSet.Tables["ChooseBrand"].Columns.Add("BrandWithCount", typeof(string), "Brand + ' (' + CountOfProducts + ')'");

Cache.Insert(strCacheName, dataSet, null, DateTime.Now.AddMinutes(21), TimeSpan.Zero);
Trace.Write("//Lägg in ChooseBrand i cachen");
}
catch (Exception objException)
{
Trace.Warn("LoadChooseBrand() Fel!", objException.Message);
}
finally
{
if (conn != null)
{
conn.Close();
conn = null;
}
}
}
return dataSet;
}

By Jesper Lind

Ny release av AJAX Control Toolkit

För en vecka sen släpptes en ny version AJAX Control Toolkit. Läs om vad som är förbättrat i denna version 10301.

Två nya kontroller finns med i paketet. Dels SlideShow som visar bilder och kan kopplas mot en webservice.

Sen även ListSearch som jag verkligen gillar. Här kan man söka i en Dropdownlista med knapptryckningar. Är en bra lösning på detta problemet som jag belyst tidigare.

By Jesper Lind

TinyMCE med AJAX och partial update

Uppdatering! Jag har nu gjort ett fullständigt exempel på hur man kan använda TinyMCE med Ajax. Länk finns här.

Har kört ASP.NET AJAX nu i en vecka och testat på UpdatePanels. Jag hade textruta med tillhörande html-editor (TinyMCE) och vid delvis uppdatering så laddades inte editorn igen. Lösningen som funkade för mig hittade jag på Moxiecode.

Först så laddar jag in själva biblioteket vid Page_Load()

//Ladda tinyMCE
HtmlGenericControl IncludeTinyMCE = new HtmlGenericControl("script");
IncludeTinyMCE.Attributes.Add("type", "text/javascript");
IncludeTinyMCE.Attributes.Add("src", "js/tiny_mce/tiny_mce.js");
this.Page.Header.Controls.Add(IncludeTinyMCE);

Efter partial upload på UpdatePanel så kallas följande funktion. Jag fick lägga anropet till den i en specialfunktion för animeringen av min UpdatePanel. Men hos Moxiecode föreslår de följande registrering.

ScriptManager.RegisterClientScriptBlock(UpdatePanel1, this.GetType(), "init", "InitTiny();", true);

I funktionen som ritar om text-editor fick jag även lägga till en extra rad som tar bort editorn efter den körs. Annars ritades en ny upp vid varje uppdatering. Det ser kanske konstigt ut att jag kör "mceRemoveControl" precis efter den lagts till men "mceAddControl", men det var det som funkade i mitt fall.

function InitTiny()
{
tinyMCE.execCommand('mceAddControl', false, 'ctl00$mainContentPH$productTabs$tpProductInfo$fvProduct$textBox');
tinyMCE.execCommand('mceRemoveControl', true, 'ctl00$mainContentPH$productTabs$tpProductInfo$fvProduct$textBox');
}

Sökvägen ser ganska lång ut men det beror på att min textarea låg inne i en Tabs-kontroll.

By Jesper Lind

Mer om det dåliga stödet för URL-omskrivning i Asp.Net

Jag älskar att utveckla i Asp.Net C# och kommer definitivt fortsätta att satsa på denna teknik. Men allt kan ju inte vara perfekt. Den största bristen i ramverket är enligt mig att det inte finns något inbyggt stöd för URL-omskrivning. Har skrivit lite om det tidigare.

Det är framför allt i IIS 6 som bristerna finns. I den nyare IIS 7 som kommer lanseras med Longhorn server kommer det finnas bättre möjligheter till att lägga in regler för URL-omskrivning. Det är ju dock en tag kvar tills det nya operativsystemet finns på marknaden så det hjälper oss inte just nu.

Det man kan göra i dagsläget är att använda ASAPI-filter men då förutsätts att du har en egen dedikerad server eller ligger på ett webbhotell som vill installera det åt dig. Jeff Atwood på Coding Horror skriver mer om de två vanligaste ASAPI-lösningarna för IIS.

En av de främsta kritikerna av situationen är nog Mike Schinkel. Detta är killen som ligger bakom initiativet wiki.welldesignedurls.org och den medföljande bloggen där man kan läsa mycket intressant på ämnet.

På sin personliga blogg sparar inte Mike på krutet. I en post nyligen med rubriken "IIS 7.0: Too Little, Too Late?" hoppar även en annan Mike (Program Manager för Microsoft’s IIS team) in i diskussionen och beklagar situationen.

Den första Mike fortätter i ett annat inlägg att lista upp alla alternativa server-tekniker och frågar sina läsare vilken han ska börja använda istället för Asp.Net. Han verkar ha tröttnat och det är lite tråkigt tycker jag.

Men bra att många uppmärksammar problemet. Microsoft har dock meddelat att IIS 7 kommer inte finnas tillgänglig för Windows 2003 och det är riktigt synd. De nya teknikerna är för djupt förankrade i Longhorns nya ramverk, menar man.

Men vem vet, efter alla böner från utvecklare kanske Microsoft kan avsätta lite resurser på det. Det skulle vara helt underbart. Mitt tips är att göra en lättare uppgradering till IIS 6 och försöka få till samma implementering av URL-omskrivning som i den kommande webbservern. Kalla den typ IIS 6.5.

By Jesper Lind

Sortera och selektera DataTable

Här är en liten kort lösning på hur man väljer ut ett visst antal rader ur en DataTable och efter en definerad sortering. Ungefär som man gör i SQL-uttrycket (SELECT TOP).

Detta hade jag haft en del problem med tidigare i samband med hantering av ett DataSet. Idag lyckades jag genom att forska i det på bland annat DeveloperFusion, TheScripts och MSDN - DataTable.Select().

Jag hade satt ihop ett antal DataSet till ett kombinerat DataSet. När jag försökte iterera igenom detta sammasatta DataTable, kom alltid de rader jag hade lagt till först, överst i utskriften. Jag behövde ha sorteringen efter en specifik parameter som var gemensam för hela den nya datasamlingen, i mitt fall en datum-kolumn.

Här är lösningen som jag kom fram till. Den använder DataTable.Select() och lämnar första parametern tom. Den andra anges för att få rätt sortering. En array av DataTable-rader returneras. Dessa importerar jag en efter en till en klonad modell av DataTablen. Jag begränsar detta till 10 omgångar. Slutligen returneras en DataView.

DataView SelectTopSortedFromDataTable(DataTable objTable)
{
DataTable outputTable = objTable.Clone();

DataRow[] foundRows = objTable.Select("","PublishDate DESC");

for(int i=0;i<10;i++)
{
DataRow objDataRow = foundRows[i];
outputTable.ImportRow(objDataRow);
}

DataView dataView = outputTable.DefaultView;
return dataView;
}

By Jesper Lind

Ladda upp och förminska bilder med ASP.NET

Vi kör ett litet torsdagstema med bildbehandling. Skrev för ett tag sen lite kortfattat om hur man kan ladda upp filer med ASP.NET. Ser att det ramlar in en hel del folk genom sökningar så tänkte gå vidare på ämnet.

Nu tänkte jag bjuda på en till mesta del hemmasnickrad klass för att ladda upp och förminska bilder till servern. Kvaliteten blir ibland inte perfekt så denna funktion passar bäst till att skapa mindre tumnaglar.

Sen har jag även märkt att röda nyanser blir något flammiga ibland. Själva funktionen "CreateThumbnail" har inte jag skrivit men har haft den så länge att jag glömt bort var den kommer från. Om någon har en bättre lösning som ger bättre resultat är jag mycket intresserad. Annars kanske det få bli en djupdykning i källkoden för gratisprogrammet Paint.NET i jakt på bätte förminskningsfunktioner.

Men nu till exemplet. Ber om ursäkt för dålig dokumentation och en blandad kompott på svenska och engelska. Använd den precis som du vill. Jag uppskattar länkar tillbaks och även om du gör några framsteg på bildkvaliteten.

Så här anropar man klassen:

if (fu.HasFile)
{
Trace.Write("We found a image to upload");
HttpPostedFile userPostedFile = fu.PostedFile;
ImageUploader imageUploader = new ImageUploader("C:/sökväg/på/server/", userPostedFile, "Namnpåbild.jpg");
//Utlämna mått för att behålla orginalstorlek på bilden
imageUploader.IntMaxWidth = 150;
imageUploader.IntMaxHeight = 50;
bool boolStatus = imageUploader.UploadImage();
if(boolStatus)
Trace.Write("Image uploaded!");
}

Och här är själva klassen:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Drawing;
using System.Drawing.Imaging;


/// <summary>
/// Summary description for ImageUploader
/// </summary>
public class ImageUploader : Page
{

//Lokala variabler
private HttpPostedFile userPostedFile = null;
private string strWhereStatement = null;
private string strRootPath = string.Empty;
private string strImageName = string.Empty;
private int intMaxWidth;
private int intMaxHeight;

//Properties
public string StrRootPath
{
get { return strRootPath; }
set { strRootPath = value; }
}
public string StrImageName
{
get { return strImageName; }
set { strImageName = value; }
}
public int IntMaxWidth
{
get { return intMaxWidth; }
set { intMaxWidth = value; }
}
public int IntMaxHeight
{
get { return intMaxHeight; }
set { intMaxHeight = value; }
}


//I första konstruktorn använder vi filens orginalnamn
public ImageUploader(string StrRootPath,HttpPostedFile UserPostedFile)
{
strRootPath = StrRootPath;
userPostedFile = UserPostedFile;
//Ta bort eventuell filändelse
strImageName = userPostedFile.FileName.Remove(userPostedFile.FileName.LastIndexOf("."));
//UploadImage(userPostedFile);
}
//I den utökade så skickar vi med ett eget filnamn
public ImageUploader(string StrRootPath,HttpPostedFile UserPostedFile,string StrImageName)
{
strRootPath = StrRootPath;
userPostedFile = UserPostedFile;

strImageName = StrImageName;
}



public bool UploadImage()
{
bool tooHigh = false;
bool tooWide = false;
bool doUpload = false;
bool uploaded = false;
string strFileExt = string.Empty;

try
{

//Kontrollera att filen är större än 0KB
if (userPostedFile.ContentLength > 0)
{

//Check på filändelse och om man ska tillåta uppladdning

//Jpg och Jpeg
if (userPostedFile.ContentType.Equals("image/jpeg") || userPostedFile.ContentType.Equals("image/pjpeg"))
{
doUpload = true;
strImageName += ".jpg";
}
//Gif
if (userPostedFile.ContentType.Equals("image/gif"))
{
doUpload = true;
strImageName += ".gif";
}
//Bmp
if (userPostedFile.ContentType.Equals("image/bmp"))
{
doUpload = true;
strImageName += ".bmp";
}
//Png
if (userPostedFile.ContentType.Equals("image/png"))
{
doUpload = true;
strImageName += ".png";
}

System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(userPostedFile.InputStream, false);

//Kolla höjd
if (intMaxHeight != null)
{
if (bitmap.Height > intMaxHeight)
{
doUpload = true;
tooHigh = true;
}
}

//Kolla bredd
if (intMaxWidth != null)
{
if (bitmap.Width > intMaxWidth)
{
doUpload = true;
tooWide = true;
}
}

Trace.Write(strRootPath + strImageName);
if (doUpload && bitmap != null)
{
Trace.Write("doUpload!!");

//Förminska om den är för stor, och ladda upp
if (tooHigh || tooWide)
{
Trace.Write("tooBig!!");
//Skapa liten automagiskt
double photoWidth, photoHeight;
double percentageDifference = 0;

photoWidth = intMaxWidth;


//Nya mått på bilden.
percentageDifference = bitmap.Width / photoWidth;
photoHeight = bitmap.Height / percentageDifference;

//Kolla även höjden
if (photoHeight > intMaxHeight)
{
photoHeight = intMaxHeight;
percentageDifference = bitmap.Height / photoHeight;
photoWidth = bitmap.Height / percentageDifference;
}

Bitmap outputBitMap = CreateThumbnail(bitmap, Convert.ToInt32(photoWidth), Convert.ToInt32(photoHeight));

//Kolla vad det är för format
ImageFormat imageFormat = outputBitMap.RawFormat;

//Spara bilden
outputBitMap.Save(@"" + strRootPath + strImageName, imageFormat);
uploaded = true;
}
//Ladda upp orginalbilden om den är inom måtten
else
{
Trace.Write("Rätt storlek eller mindre.");


userPostedFile.SaveAs(@"" + strRootPath + strImageName);
uploaded = true;
}

}

}
}
catch(Exception objException)
{
Trace.Write("Upload fel!", objException.Message);
uploaded=false;
}
return uploaded;
}

//Tar emot en BitMap och gör den mindre
public static Bitmap CreateThumbnail(Bitmap loBMP, int lnWidth, int lnHeight)
{
System.Drawing.Bitmap bmpOut = null;

try
{

ImageFormat loFormat = loBMP.RawFormat;
decimal lnRatio;
int lnNewWidth = 0;
int lnNewHeight = 0;

//Om bilden är större än en thumbnail, returnera den.
if (loBMP.Width < lnWidth && loBMP.Height < lnHeight)
return loBMP;

if (loBMP.Width > loBMP.Height)
{
lnRatio = (decimal)lnWidth / loBMP.Width;
lnNewWidth = lnWidth;
decimal lnTemp = loBMP.Height * lnRatio;
lnNewHeight = (int)lnTemp;
}
else
{
lnRatio = (decimal)lnHeight / loBMP.Height;
lnNewHeight = lnHeight;
decimal lnTemp = loBMP.Width * lnRatio;
lnNewWidth = (int)lnTemp;
}

bmpOut = new Bitmap(lnNewWidth, lnNewHeight);
Graphics g = Graphics.FromImage(bmpOut);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.FillRectangle(Brushes.White, 0, 0, lnNewWidth, lnNewHeight);
g.DrawImage(loBMP, 0, 0, lnNewWidth, lnNewHeight);
loBMP.Dispose();
}
catch
{
return null;
}
return bmpOut;
}
}

By Jesper Lind