2009-02-16

Aldrig Fluent Interface på bekostnad av god design

Johan Lindfors har ett blogginlägg om hur man kan instansiera ett objekt, en KaffeLatte, med fluent interface, eller member chaining. Eftersom han ville bolla idéen så kommer här min replik på det. Så här ser Johans exempel ut:

KatteLatte latte = KatteLatte.beställ
.dubbel
.fettfri
.vanilj;


Klassen måste då se ut så här:




public class KatteLatte
{
public static KatteLatte beställ
{
get
{
return new KatteLatte();
}
}

public KatteLatte dubbel
{
get
{
this.Dubbel = true;
return this;
}
}

public KatteLatte vanilj
{
get
{
this.Vanilj = true;
return this;
}
}

public KatteLatte fettfri
{
get
{
this.Vanilj = true;
return this;
}
}

public bool Vanilj { get; private set; }
public bool Dubbel { get; private set; }
public bool Fettfri { get; private set; }
}


Inte så snyggt, om du frågar mig, och andra invänder mot det i kommentarerna till Johans inlägg. Fluent Interface är trevligt, men det får inte ske på bekostnad av principer för god objekt orienterad programmering.



I det här fallet är latten ett typexempel på Decorator Pattern (just kaffe finns med som exempel på Wikipedia). Jag går inte in på varför det är en bättre lösning, utan konstaterar bara att det är det. :)



Vidare bör man inte blanda ihop det man skapar med hur man skapar det. Här kommer ett exempel på betydligt bättre design OCH fluent interface.



Om vi börjar med Decorator-mönstret. I korthet kan man säga att man kedjar ihop liknande objekt med varandra. Dessa kan hakas ihop i valfri ordning och man kan därmed bygga både komplexa objekt och nya strukturer man inte förutsåg när man började. I det här fallet skapar vi upp de olika ingredienserna och med hjälp av mönstret kan vi haka ihop dessa till olika drycker.




public abstract class Dryck
{
private readonly Dryck _dryck;

protected Dryck() { }
protected Dryck(Dryck dryck) { _dryck = dryck; }
…
}

public class Espresso : Dryck
{
public Espresso() { }
public Espresso(Dryck dryck) : base(dryck) { }
…
}

public class Mjölk : Dryck
{
public Mjölk() { }
public Mjölk(Dryck dryck) : base(dryck) { }
…
}

public class FettfriMjölk : Dryck
{
public FettfriMjölk() { }
public FettfriMjölk(Dryck dryck) : base(dryck) { }
…
}
public class Vanilj : Dryck
{
public Vanilj() { }
public Vanilj(Dryck dryck) : base(dryck) { }
…
}

… fler drycker


Vi kan nu skapa lite olika drycker så här:




var latte=new Espresso(new Milk());
var dubbelEspresso=new Espresso(new Espresso());
var americano=new Espresso(new Vatten());


(Mängden mjölk i en latte kan diskuteras, men om du frågar mig är det alltid för mycket mjölk i en kaffe med mjölk i :)



Sen skapas en Abstract Factory (jag tog bort dubbel för att göra det hela lite enklare). Fabriken har till uppgift att skapa en dryck.




public interface IDryckFactory
{
IDryckFactory Vanilj();
IDryckFactory Fettfri();
Dryck BeställningKlar();
}

public class LatteFactory : IDryckFactory
{
private bool _vanilj;
private bool _fettfri;

public static IDryckFactory Beställ()
{
return new LatteFactory();
}

public IDryckFactory Vanilj() { _vanilj = true; return this; }
public IDryckFactory Fettfri() { _fettfri = true; return this; }
public Dryck BeställningKlar()
{
Dryck latte = _fettfri ?
new Espresso(new FettfriMjölk()) :
new Espresso(new Mjölk());
if(_vanilj)
{
latte = new Vanilj(latte);
}
return latte;
}
}


För att skapa en latte nu så kan man skriva:




var latte = LatteFactory.Beställ()
.Fettfri()
.Vanilj()
.BeställningKlar();


Inte så mycket mer för utvecklaren som ska skapa en latte att skriva, men mycket, mycket bättre underliggande design.



Mönstret <Skapa ett objekt, anropa lite metoder, avsluta med en “avsluta”-metod", få ut ett resultat> går att använda till fler saker. Metoden fungerar bra till valideringar t.ex




string myString;
...
bool isValid=myString.StartValidation().
IsNotNull().
LengthIsAtLeast(5).
LengthIsAtMost(10).
DoesNotContain("-").
Validate();


Går inte in i detalj på hur man gör, kanske kommer det en post om det i framtiden, men här används extension metoder och generiska klasser på ett listigt sätt. Det går att utöka så att man, utöver en bool, kan få ut felmeddelanden om varje del som inte validerar.

2008-07-27

Inkludera script efter asp.net ajax-ramverkets filer

När man registrerar script-filer med ScriptManager.RegisterClientScriptInclude("myFile.js") inkluderas alltid filerna före ajax-ramverkets. Det är inte vad man vill om filerna är beroende av ramverket.Oftast märker man det på att man får script-fel liknande "Type is not defined" då man oftast överst i en sina filer deklarerar ett namespace med: "Type.registerNamespace('My.Namespace");"

Om man däremot registrerar en fil som en referens via en ScriptManagerProxy, kommer filen att inkluderas efter ramverkets:

<asp:ScriptManagerProxy runat="server" id="ScriptManagerProxy1">
<Scripts><asp:ScriptReference Path="~/anotherFile.js" /></Scripts>
</asp:ScriptManagerProxy>


Förklaringen till betendet är enkel, även om man önskar att det inte hade fungerat så här.



När man registrerar filen via RegisterClientScriptInclude kommer funktionalitet i en klass som heter ClientScriptManager att användas. ClientScriptManager har internt en lista med script-filer och filer som registrerars hamnar sist i den listan. Så filen kommer, direkt när man anropar ScriptManager.RegisterClientScriptInclude("myFile.js"), att hamna i listan. Efter anropet kommer listan att se ut så här:



ClientScriptManagerClientScripts = { ..., "~/myFile.js" }


Filerna i ScriptManagerProxy hanteras däremot lite annorlunda. ScriptManager registrerar sig på eventet PagePreRenderComplete och när det inträffar samlar ScriptManager ihop filerna från alla ScriptManagerProxies i en lista. Först i den listan hamnar ajax-ramverkets filer.



Lista hos ScriptManager = { ajax-ramverkets filer, "~/anotherFile.js" }


Dessa registreras hos ClientScriptManager och hamnar då i dess lista. Eftersom vi har registrerat myFile.js för det att PagePreRenderComplete inträffar (PagePreRenderComplete inträffar sent i asp.net pipeline) så kommer myFile.js att ligga före ramverkets:





ClientScriptManagerClientScripts = { ..., "~/myFile.js", ..., ajax-ramverkets filer, "~/anotherFile.js"}


Dessa skrivs sedan ut till sidan under Render-fasen och det förklarar varför anotherFile.js har tillgång till ramverkets kod och myFile.js inte har det.



Lösning 1. Använd proxy



Den enklaste lösningen är att lägga in en ScriptManagerProxy på sidan/user control och antingen, som ovan, registrera en ScriptReference, eller göra det via kod genom:



ScriptManagerProxy1.Scripts.Add(new ScriptReference("~/myFile.js"));


Lösning 2. Registrera efter PagePreRenderComplete



Om man på något sätt lyckas registrera myFile.js efter PagePreRenderComplete kommer filen att hamna efter ScriptManagerns filer. Det gäller dock att hamna före rendreringen av script-blocken. Alltså: efter PagePreRenderComplete men före Render. Det som händer mellan dessa faser är sparning av ViewState  Så om vi i SaveViewState registrerar myFile.js kommer den att hamna efter ajax-ramverkets filer. Det är lite hackigt, och inte garanterat att fungera när nya versioner av ramverket kommer, men: det fungerar.



protected override object SaveViewState()
{
ScriptManager.RegisterClientScriptInclude(this,GetType(),"myFile","~/myFile.js");
return base.SaveViewState();
}


Lösning 3. En egen ScriptManager



Det här är i mina ögon den snyggaste lösningen. Man skapar klassen MyScriptManager som ärver av ScriptManager och byter ut ScriptManager på sidan mot MyScriptManager. På MyScriptManager lägger man till en metod RegisterClientScriptInclude(string url) och genom lite finurligheter internt i MyScriptManager kan man få filer som registreras via den nya metoden att hamna efter ajax-ramverkets.



När ScriptManager samlar ihop filer från samtliga ScriptManagerProxies så gör den dessutom ytterligare en sak: Den hämtar script-filer från kontroller som registrerats via ScriptManager.GetCurrent(Page).RegisterScriptControl. Dessa kommer att inkluderas efter proxy-filerna och därmed även efter ramverks-filerna.



Om vi håller reda på de filer som registrerats via den nya metoden i en egen lista och låter vår MyScriptManager vara en IScriptControl och registrera sig själv som en ScriptControl kan vi få in vår lista med filer efter ramverksfilerna. Det är inte så mycket kod som behövs:



public class MyScriptManager : ScriptManager, IScriptControl
{
private List<string> _registeredScripts = new List<string>();

public virtual void RegisterClientScriptInclude(string url)
{
_registeredScripts.Add(url);
}

protected override void OnPreRender(EventArgs e)
{
//Register this instance as a ScriptControl.
RegisterScriptControl(this);
base.OnPreRender(e);
}

public new static MyScriptManager GetCurrent(Page page)
{
return (MyScriptManager) ScriptManager.GetCurrent(page);
}

#region IScriptControl Members
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
return null;
}

IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
//For each element in _registeredScripts create a
//ScriptReference and return the IEnumerable

return _registeredScripts.ConvertAll(s => new ScriptReference(s));
}
#endregion
}


Observera att koden ovan är förenklad så långt det går för att visa konceptet. I produktionskod vill man t.ex. inte skapa listan om den inte behövs. Dessutom bör man skapa två protected virtual varianter av GetScriptDescriptors och GetScriptReferences som IScriptControl.GetScriptDescriptors resp. IScriptControl.GetScriptReferences får anropa.



Med följande kodrad registreras en script-fil som kommer att hamna efter ajax-ramverkets:



MyScriptManager.GetCurrent(Page).RegisterClientScriptInclude("~/myFile.js");


Om du använder AjaxControlToolkit ändrar du bara arvet till ToolkitScriptManager.



Observera att filordningen blir:




  1. ScriptManager.RegisterClientScriptInclude-registrerade filer


  2. Ajax-ramverkets filer


  3. Alla ScriptManagerProxy-filer


  4. MyScriptManager.RegisterClientScriptInclude-registrerade filer



Om du vill få in filer mellan punkt 2 och 3 får du problem. Det går säkert att göra, men det är antagligen inte trivialt eftersom ScriptManager är rätt stängd för modifiering. Det hade varit trevligt om Microsoft följt Open/Closed-principen lite mer.

2008-05-17

Bygga Database Project i TFS

Att få igång byggen på Team Foundation Server 2008 med Team Build där Database Project ingår i en solutionfil är inte helt enkelt. Själv har jag inte kört Database Project något, än. I projektet jag jobbar i behövde vi få igång dagliga byggen så det föll på min lott att få det att funka.

Utöver att kompilera så bygger vi och deployar databas och webappen till en server. Det här inlägget beskriver vad vi gjorde för att få igång det. Observera att vi inte kickat igång unit-tester än, så jag vet inte hur det här fungerar ihop med det.

Skapa ett vanligt bygge

Steg ett är att skapa ett bygge i TFS 2008 på vanligt sätt och peka ut solution-filen man vill bygga.

Ange databas där bygget ska ske

När man bygger ett dabasprojekt i Visual Studio 2008 så byggs databasen enl. de inställningar man gjort i VS för det projektet. Vi låter varje utvecklare köra mot en egen instans av Sql. Dessa inställningar lagras inte i dbproj-filen, utan i en fil som slutar på dbproj.user. Denna fil är unik för varje användare och checkas därför inte in i Source Control.

På något sätt måste vi ersätta det som står i dbproj.user-filen och tala om för byggservern var den ska bygga någonstans. Det är TargetDatabase, TargetConnectionString, eller DefaultDataPath som måste anges och det finns lite olika sätt att göra det på. Buck Hodges har beskrivit hur man får igång byggen och han nämner 3 sätt att göra det på (två i texten och en i kommentarerna).

  1. Ange värdena direkt i .dbproj-filen
    Checka ut dbproj-filen, öppna den som xml-fil och lägg in värdena för fälten. Enklast är att modifiera värdena från dbproj.user-filen.
    <PropertyGroup Condition=" '$(Configuration)' == 'Default' ">
    <TargetDatabase>MyProject_Daily</TargetDatabase>
    <TargetConnectionString>Data Source=.\SQLEXPRESS;Integrated Security=True;Pooling=False</TargetConnectionString>
    <DefaultDataPath>d:\data\</DefaultDataPath>
    </PropertyGroup>



  2. Skicka in värdena som command line-argument till MSBuild, antingen i Queue Build-fönstret eller genom att lägga dem TFSBuild.rsp-filen. T.ex.
    /p:DefaultDataPath=<path>; TargetDatabase=<databaseName>;TargetConnectionString="<connection string>"



  3. Skapa en egen targets-fil och inkludera den precis innan dbproj.user importeras.

    Skapa en fil .dbproj.BuildDefaults:

    <?xml version="1.0" encoding="utf-8"?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup Condition=" '$(Configuration)' == 'Default' ">
    <TargetDatabase>MyProject_Daily</TargetDatabase>
    <TargetConnectionString>Data Source=.\SQLEXPRESS;Integrated Security=True;Pooling=False</TargetConnectionString>
    <DefaultDataPath>d:\DATA\</DefaultDataPath>
    </PropertyGroup>
    </Project>

    Lägg sedan till de två översta raderna nedan i dbproj filen precis innan importen av Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets.

    <!-- Import default settings -->
    <Import Condition="Exists('$(MSBuildProjectFullPath).BuildDefaults')" Project="$(MSBuildProjectFullPath).BuildDefaults" />
    <!--Import the settings-->
    <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\TeamData\Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets" />

    Eftersom Microsoft.VisualStudio.TeamSystem.Data.Tasks.targets importerar dbproj.user-filen (om den finns) så kommer det som finns i BuildDefaults att ersättas av det som står i dbproj.user filen när man bygger lokalt. På byggservern finns inte dbproj.user-filen och därför kommer värdena från BuildDefaults-filen att gälla.



Alternativ 1 och 3 innebär att man (i princip) talar om i källkoden var man ska bygga. Inte så snyggt. Inte så flexibelt. Bygg-inställningarna vill man hålla borta från koden i enlighet med Separation of Concerns-tänket.



Alt. 2 är rätt ok, men jag föredrar att hålla alla bygg-inställningar i TFSBuild.proj-filen  Det enklaste är faktiskt att skicka med parametrarna när man specificerar vilken solution-fil som ska byggas (i TFSBuild.proj):



<SolutionToBuild Include="$(BuildProjectFolderPath)/../../MyProject.sln">
<Targets></Targets>
<Properties>TargetDatabase=MyProject_Daily;DefaultDataPath=e:\data;TargetConnectionString=Data Source=.\SQLEXPRESS</Properties>
</SolutionToBuild>



HACK: Observera Target Connection-strängen. Jag utnyttjar default-värden för Integrated Security. Om man lägger in semikolon, oavsett om man gör det som ; eller %3B kommer dessa värden att tolkas som egna properties med skumma fel som följd.




Vi har ställt in solution-filen på att bygga databas projektet även för Debug och Release. Om man inte gör det måste man se till att bygga Default-flavor också (Buck Hodges förklarar under Issue #3).



Nu ska allt bygga. Om du får problem, kolla Buck Hodges text.



Deploya



För att deploya databasen till en server måste man bygga databasen för den servern också. Vi har valt att köra ytterligare ett byggomgång för databasprojeket och sen efter att allt droppats så deployar vi.



Bygg för deploy



För att bygga databasprojektet använder man en MSBuild Task:



<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Rebuild" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>


För att få den att köra måste man lägga den i en Target och se till att den exekveras ihop med den övriga kompileringen:



<Target Name="AfterCompile" DependsOnTargets="$(AfterCompile);BuildDatabaseForDeploy"/>
<Target Name="BuildDatabaseForDeploy">
<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Rebuild" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>
</Target>


Deploy



För att deploya till en sql server kör man MSBuild igen, men med Deploy som Targets. Detta görs efter det att bygget droppats.



<Target Name="AfterDropBuild" DependsOnTargets="$(AfterDropBuild);DeployDatabase" />
<Target Name="DeployDatabase" DependsOnTargets="BuildDatabaseForDeploy">
<MSBuild Projects="$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj" Targets="Deploy" Properties="TargetDatabase=MyDb; TargetConnectionString=Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B;DefaultDataPath=d:\data\"/>
</Target>


Snygga till det hela



Koden ovan är det minimala man måste göra, för att få det hela lite snyggare bör man samla alla inställningar på ett ställe. När man bygger för deploy så skapas ingen sql-fil i drops-katalogen. Man får heller inget meddelande om att stegen körs. Nedanstående fixar till det. Lägg in det före </Project> i TFSBuild.proj.



<PropertyGroup>
<DbDeployTargetConnectionString Condition=" '$(DbDeployTargetConnectionString)'==''">Data Source=MyServer%3BIntegrated Security=True%3BPooling=False%3B</DbDeployTargetConnectionString>
<DbDeployTargetDatabase Condition=" '$(DbDeployTargetDatabase)'==''">MyDb</DbDeployTargetDatabase>
<DbDeployDefaultDataPath Condition=" '$(DbDeployDefaultDataPath)'==''">d:\data\</DbDeployDefaultDataPath>
<DbDeployBuildScriptName Condition=" '$(DbDeployBuildScriptName)'==''">$(DbDeployTargetDatabase)_Deploy.sql</DbDeployBuildScriptName>
<DatabaseProjectFilePath>$(SolutionRoot)/Database/MyDatabase/MyDatabase.dbproj</DatabaseProjectFilePath>
</PropertyGroup>

<PropertyGroup>
<DbForDeployProperties Condition=" '$(DbForDeployProperties)'==''">
TargetDatabase=$(DbDeployTargetDatabase);
TargetConnectionString=$(DbDeployTargetConnectionString);
DefaultDataPath=$(DbDeployDefaultDataPath);
BuildScriptName=$(DbDeployBuildScriptName);
OutputPath=$(BinariesRoot)
</DbForDeployProperties>
</PropertyGroup>

<Target Name="AfterCompile" DependsOnTargets="$(AfterCompile);BuildDatabaseForDeploy"/>
<Target Name="BuildDatabaseForDeploy">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="Building database for deployment.">
<Output TaskParameter="Id" PropertyName="StepId" />
</BuildStep>
<MSBuild Projects="$(DatabaseProjectFilePath)" Targets="Rebuild" Properties="$(DbForDeployProperties)"/>
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Succeeded" />

<OnError ExecuteTargets="DbMarkBuildStepAsFailed" />
</Target>

<Target Name="AfterDropBuild" DependsOnTargets="$(AfterDropBuild);DeployDatabase" />
<Target Name="DeployDatabase" DependsOnTargets="BuildDatabaseForDeploy">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="Deploying database.">
<Output TaskParameter="Id" PropertyName="StepId" />
</BuildStep>
<MSBuild Projects="$(DatabaseProjectFilePath)" Targets="Deploy" Properties="$(DbForDeployProperties)"/>
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Succeeded" />

<OnError ExecuteTargets="DbMarkBuildStepAsFailed" />
</Target>

<Target Name="DbMarkBuildStepAsFailed">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StepId)"
Status="Failed" />
</Target>




Så här ser det ut när det byggs:

Steps .



Att deploya applikationen är en annan historia. Vi deployar webapparna med en Copy Task och sen har vi en egenutvecklad Task som ändrar värden i web.config-filerna så att det passar miljön applikationen ligger i.

2008-02-19

C# 3.0: Automatiskt implementerade egenskaper

Jag tänkte jag skulle beskriva lite om nyheterna i C# 3.0. Det finns väl en hel del av dessa beskrivningar ute på nätet nu, men jag har inte sett någon riktigt bra på svenska som gått lite mer på djupet.

Först ut är automatiskt implementerade egenskaper (eng. auto-implemented properties).

C# 2.0

I C# 2.0 skrivs properties så här:

private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}



C# 3.0


Självklart kan du fortsätta skriva properties på samma sätt som i 2.0, men eftersom många properties är just enkla hämta-lagra-ett-värde så finns det i C# 3.0 ett mer kompakt sätt att skriva ovanstående på.

public string Name { get; set; }

Båda varianterna är i princip ekvivalenta. Kompilatorn kommer automatiskt att generera en privat variabel, som sen används på samma sätt som _name. Den lägger dessutom till attributet CompilerGenerated för att markera att get- och set-metoderna är genererade av kompilatorn.


Sidospår: En property består i själva verket av en get-metod och en set-metod. Så Name har en metod: string get_Name() och en void set_Name(string value). Det är båda dessa som markeras med attributet CompilerGenerated.


På samma sätt som i 2.0 kan man ange private/protected på set för att göra en, utifrån sett, read-only property:

public string Name { get; private set; }
public string Name2 { get; protected set; }

Observera att man måste ha både get och set. Så följande är inte tillåtet:


public string Name { get; }
public string Name2 { set; }

Varför ska man kunna hämta ett värde när man inte kan sätta det, och vice-versa?


Tips: I Visual Studio skapar man properties enklast genom att skriva prop och trycka tab två gånger.


Läs mer


Kortfattad summering:
http://msdn2.microsoft.com/en-us/library/bb384054.aspx
Scott Guthrie förklarar:
http://weblogs.asp.net/scottgu/archive/2007/03/08/new-c-orcas-language-features-automatic-properties-object-initializers-and-collection-initializers.aspx
Detaljerna kring vad som händer under skalet:
http://community.bartdesmet.net/blogs/bart/archive/2007/03/03/c-3-0-automatic-properties-explained.aspx

2008-01-03

Currying i Javascript

En trevlig sak som finns i funktionella språk är currying och partial application, vilket lite förenklat kan förklaras som att om man anropar en funktion med färre argument än vad funktionen kräver får man tillbaka en ny funktion som kräver de resterande argumenten (egentligen står currying för själva transformerandet av en funktion till en annan).

Anta att vi har en funktion add som givet två tal, a & b, adderar dem med varandra.

function add a b -> a+b



Då kan du skapa en ny funktion increase som alltid adderar 1 till ett tal så här

increase = add (1)
increase (7) ==> ger resultatet 8



Trivialt exempel, men det visar att man på det här sättet kan låta funktioner vara byggblock som kan återanvändas och ingå i andra funktioner på enkelt sätt.


Svend Tofte har en intressant artikel om hur detta kan göras i Javascript (han har även ett exempel som visar när currying kan vara användbart): http://www.svendtofte.com/code/curried_javascript/


 


Och, ja man kan fippla till det även i C#: http://diditwith.net/2007/08/15/TheArtOfCurrying.aspx

2007-10-09

Event som inte höjs för dynamiskt skapa kontroller

En kollega hade problem med att ett Click-event för en dynamiskt skapad kontroll inte höjdes vid vissa postbacks. Oftast brukar problem med att event inte höjs för en dynamiskt skapad kontroll hänga ihop med att dess ID ändras mellan postbacks. Så även denna gång.

Sidan och problemet

Förenklat sett, består sidan av en lista med saker och en dropdownlist som används som val för att filtrera listan. Initialt visar listan upp alla saker. I slutet på varje rad i listan finns en knapp (en ImageButton). Saker ska hända när man klickar på knappen genom en eventhanterare på knappens Click-event,

När man visar sidan för första gången visar listan alla saker och ett klick på en av knapparna leder till att eventet höjs.

När man väljer något i dropdownlisten så autopostbackar sidan och listan populeras om. Klickar man nu på någon knapp görs en postback men eventet höjs aldrig. Sidan returneras. Ett klick på en knapp nu fungerar; eventet höjs.

Så direkt efter att man gjort ett val slutar eventet att höjas, för att vi nästa försök fungera igen.

Hur listan byggs upp

Listan byggs upp i en metod MyCreateControls() som anropas i Page_Load. I metoden hämtas en lista med saker att visa. För varje sak i listan laddas en UserControl, lite värden sätts på UserControl-instansen och den läggs till en PlaceHolders ControlCollection via propertyn Controls.

Pseudokod för MyCreateControls

MyCreateControls()
PlaceHolder1.Controls.Clear()
DataList=GetData()
ForEach(item in DataList)
{
ctrl=MyCreateControl()
ctrl.data=item.data
PlaceHolder1.Controls.Add(ctrl)
}



När man väljer ett item i dropdownlistan anropas MyCreateControls. PlaceHoldern rensas på alla kontroller med Controls.Clear() (PostBackEvents höjs efter OnLoad som redan hunnit fylla på listan enligt de gamla kriterierna) och därefter fylls PlaceHoldern på som beroende på valet i dropdownlistan kommer att använda en annan lista av saker.


UniqueID


Varje kontroll, och då menar jag Control, har en property UniqueID med ett id som är unikt för just den kontrollen. Detta id består av två delar: den första delen är UniqueID för den NamingContainer som kontrollen ligger i; den andra delen är kontrollens ID. Så för en kontroll med ID="MyChildControl" som ligger i en NamingContainer-kontroll med UniqueID="MyParent" är UniqueId="MyParent:MyChildControl". Separatorn kan också vara "$".


Om kontrollen saknar id används en löpnummerserie i stil med "ctl00", "ctl01" och vi får då UniqueId="MyParent:ctl00". Skapandet av ID om kontrollen saknar ett görs i Control.GenerateAutomaticID() som, för varje gång den behöver skapa ett unikt id, räknar upp ett värde på kontrollens NamingControl. Så lägger vi till tre kontroller som saknar ID till en NamingControl kommer här värdet att räknas upp från 0 till 1 till 2 till 3 (den sista kontrollen får ID="ctl02"). Detta värde nollställs eller räknas aldrig ner så tar vi sen bort dessa tre kontroller och lägger dit en ny, som också saknar ID kommer den att få ID="ctl03" och värdet har räknats upp till 4.


När vi ändå håller på: ClientId är UniqueId med "_" som separator istället för ":".  Så vi får då: "MyParent_MyChildControl"


UniqueID och PostBacks


När en kontroll, t.ex. en ImageButton (som skrivs ut som en <input type="image">-HTML-kontroll) postar tillbaka värden görs det under dess UniqueID. Så om man klickar på en ImageButton som har UniqueID="MyParent:ImageButton1" kommer den att skicka tillbaka den x- och y-koordinat man klickade på under "MyParent:ImageButton1.x" och "MyParent:ImageButton1.y". Det här gör att Asp.Net vet till vilken kontroll varje postat värde hör till.


Problemet denna gång


Tillbaka till det faktiska problement. UserControlen som lades i PlaceHoldern fick inget ID satt. Det gjorde att varje UserControl fick UniqueID i stil med: "xx:PlaceHolder1:ctl00", "xx:PlaceHolder1:ctl01" osv.


Antag att vi via Page_Load lagt till tio kontroller (den sista kontrollen får ID="ctl09"). Om man valt ett nytt värde i dropdownen kommer MyCreateControls() att anropas igen och dessa tio kontroller tas då bort ur Controls. Istället kommer till exempel tre nya kontroller att läggas till. Den första nya kontrollen blir dock inte "ctl00" utan "ctl10", eftersom det är så Control.GenerateAutomaticID fungerar: den fortsätter på löpnummerserien. Så vi har då de tre kontrollerna "ctl10", "ctl11" och "ctl12".


Vad händer då när man klickar på den första ImageButton-kontrollen som ligger i den första UserControlen? Den har UniqueID="xx:PlaceHolder1:ctl10:ImageButton1", så x- och y-koordinaten skickas i "xx:PlaceHolder1:ctl10:ImageButton1.x" och "xx:PlaceHolder1:ctl10:ImageButton1.y".


Men Page_Load som bygger upp de tre kontrollerna på nytt kommer denna gång att skapa "ctl00", "ctl01" och "ctl02" (eftersom räknaren alltid börjar på noll och det är de första kontrollerna vi lägger till). Gissa vad som händer då med våra x- och y-värden.


Absolut ingenting. Det finns ingen ImageButton1 som ligger i en kontroll med UniqueID="xx:PlaceHolder1:ctl10". Och eftersom inga x- och y-värden ändras på någon kontroll kommer inte någon ImageButton att höja sitt Click-event. Sidan skickas tillbaka.


Men, klickar man på knappen denna gång så skickas "xx:PlaceHolder1:ctl00:ImageButton1.x" och "xx:PlaceHolder1:ctl01:ImageButton1.y" och den här gången finns det en ImageButton1 som ligger i en NamingContainer med UniqueID="xx:PlaceHolder1:ctl00" vilket gör att värdena hamnar rätt. I och med att dessa värden sätts så kommer ImageButton1 att höja sitt Click-event.


Lösningen


Den enkla lösningen är att själv ange ett ID med en löpnummerserie, t.ex. C0, C1 osv.)  som nollställs i i början av MyCreateControls(). Det här säkerställer att första kontrollen alltid får id=C0, och därmed kommer PostBack-värdena att alltid hamna rätt.

MyCreateControls()
PlaceHolder1.Controls.Clear()
DataList=GetData()
Counter=0
ForEach(item in DataList)
{
ctrl=MyCreateControl()
ctrl.data=item.data
ctrl.ID="C"+Counter
PlaceHolder1.Controls.Add(ctrl)
Counter=Counter+1
}



Avslutningsvis


Om du har problem med att event höjs ibland och ibland inte, ta dig en titt på kontrollernas ID:n. Ändrar de sig mellan postbacks så har du troligen orsaken där.

2007-10-02

ID-optimering, fortsättning

Jag skrev i en tidigare bloggpost om hur man enkelt kan optimera sina sidor genom att välja korta ID:n på sina Asp.Net-kontroller. Det här är en fortsättning.

ID på kontroller som saknar ID

Vissa kontroller saknar ID:n. De får då ett ID av typen ctl00, ctl01, ctl02 och så vidare. Det kan därför vara god idé att tilldela dessa kontroller ett ID som är kortare.

ID på MasterPage

På sajter där man använder MasterPages sidor så visar det sig att alla ID:n börjar med ctl00. Med största sannolikhet så kommer den delen från själva MasterPage-kontrollen, eftersom de flesta inte bryr sig om att sätta något ID på den.


För att sätta ID på en masterpage får man gå in i dess konstruktor och där sätta ID. Om masterpage-kontrollen heter DefaultMasterPage.master, så ska följande kod läggas till i dess code behind:

    public DefaultMasterPage()
{
ID = "M";
}



Den förändringen slår i genom på alla kontroller på alla sidor (som utnyttjar DefaultMasterPage).

2007-10-01

Använd korta ID på tunga sidor

I projektet jag jobbar i nu har vi en rätt tung sida: den är komplex till sin struktur, mycket nästlade kontroller och mycket databindning.

Eftersom vi när vi byggde sidan koncentrerade oss på funktionalitet framför att få den snabb blev vi nu, såklart, tvungna att optimera sidan.

Jag har precis påbörjat arbetet. En första sak jag gjorde var att byta ut alla ID:n. Från saker som RepeaterAllRooms och ContainerValidators till RR och CV.

ID:n "ärvs"

Ju högre upp en kontroll är i hierarkin desto viktigare är det att få ID:et kort, eftersom föräldrars ID används för att skapa en kontrolls unika ID. Så alla rader i kontrollen RepeaterAllRooms får ID i stil med RepeaterAllRooms_RoomRow1, RepeaterAllRooms_RoomRow2, etc. Dessutom ligger ju de i sin tur i en container som ligger i en container som ligger i en container som…

Längst ner i hierarkin har vi input-fält. Varje fält fick ID:n i stil med:
ctl00_ctl00_ContentPlaceHolder1_tabContentPlaceHolder_RateAllotmentEditView1_
RateAllotmentControl_MultiTemplatePeriods_I0_RepeaterRooms_I0_RepeatedColumns_
ctl01_ColumnData_colPercentage_txt

Eftersom det är ett input-fält får den dessutom lika långt Name:
ctl00$ctl00$ContentPlaceHolder1$tabContentPlaceHolder$RateAllotmentEditView1$
RateAllotmentControl$MultiTemplatePeriods$I0$RepeaterRooms$I0$RepeatedColumns$
ctl01$ColumnData$colPercentage$txt

Med 100 fält blir sedan snabbt rätt tung. Inte nog med det; ViewState blir tungt också eftersom ID:n används även där.

36% html-kod försvann

Före förändringen var HTML-koden för sidan på runt 338 kB. Efter kortare-ID-bytet var sidan nere i 216 kB. Rätt stor förändring utan särskilt mycket jobb! 

Nästa steg blir bl.a. att se över ViewState.

Förkorta, förkorta, förkorta

Så se till att använda korta ID:n. Kör du med MasterPages, byt ut ContentPlaceHolder1 mot t.ex. CPH eftersom ALLA kontroller under det kommer att innehålla strängen ContentPlaceHolder1. På vår sida gav bara det en minskning med 14 kB. Kontroller som i sin tur innehåller många andra kontroller, som t.ex. en databunden Repeater, är goda kandidater för förkortning.

Att förkorta ID måste man ju såklart väga mot att få läsbar kod.

 

2007-09-15

$get och $find i Asp.Net Ajax

I Asp.Net Ajax finns två metoder som kan verka snarlika: $get och $find.

$get för DOM-element

Använd $get(id) för att få tag på ett DOM-element, t.ex. input, button, a, table, etc. som finns någonstans i dokumentet. Med $get(id,parent) söker du enbart bland barnen (inkl. barnbarn, barnbarnsbarn, etc) till parent.

$find för Sys.Component, Sys.UI.Behavior och Sys.UI.Control

Använd $find(id) och $find(id,parent) för att hitta en Asp.Net Ajax Sys.Component (t.ex. behaviors och Sys.UI-kontroller). Typiskt skapas dessa av ramverket med $create. Använd inte $find för att hitta DOM-element.

Mer information:
http://mattberseth.com/blog/2007/08/the_everuseful_get_and_find_as.html
http://blogs.msdn.com/irenak/archive/2007/02/19/sysk-290-asp-net-ajax-get-vs-find.aspx

2007-07-27

AutoEventWireup=true

Om du på en Web User Control eller sida skriver AutoEventWireup=True kommer metoder som t.ex. Page_Init() och Page_Load() att automatiskt hakas på sidans Init- resp. Load-event.

Detta är bra ibland, dumt ibland. Smidigt för man slipper haka på eventen själv. Dumt därför att det är något som görs i runtime (tar därmed tid) och kan leda till att event anropas flera gånger.

Exempel: Event-hanterare som anropas två gånger.

protected void Page_Init()
{
Page.Load += Page_Load;
}
protected void Page_Load(object sender, EventArgs e)
{
//This code will execute twice
}

I ovanstående exempel kommer Page_Load att anropas två gånger. En gång eftersom du manuellt hakat på metoden på Load-eventet. Ytterligare en gång därför att AutoEventWireup=true och metoden heter som den gör vilket gör att den automatiskt kommer att hakas på Load-eventet.


AutoEventWireup fungerar  för TemplateControl vilket både Page och Web User Controls ärver av.


Du kan, om du vill dock inte nödvändigt, börja med att läsa K Scott Allens bloggpost om vad AutoEventWireup gör. Det nedan är en fördjupning på ämnet. :)


SupportAutoEvents


Vad händer egentligen när AutoEventWireup=true? En TemplateControl har en property som heter SupportAutoEvents. Denna är true per default (och ändras bara till false av parsern/control buildern som bygger upp kontrollen om man sätter AutoEventWireup=false).


HookUpAutomaticHandlers


TemplateControl.HookUpAutomaticHandlers (vilken anropas under kontrollens OnInit bland annat) kommer om AutoEventWireup=true att försöka hitta följande metoder:



  • Om kontrollen är Page:

    • Page_PreInit
    • Page_PreLoad
    • Page_LoadComplete
    • Page_PreRenderComplete
    • Page_InitComplete
    • Page_SaveStateComplete

  • För alla TemplateControls:

    • Page_Init
    • Page_Load
    • Page_DataBind
    • Page_PreRender
    • Page_Unload
    • Page_Error
    • Page_AbortTransaction eller om den inte finns; OnTransactionAbort
    • Page_CommitTransaction eller om den inte finns; OnTransactionCommit

Detta görs i metoden TemplateControl.GetDelegateInformationWithNoAssert().


32 försök


Som K Scott Allen skriver kommer den för varje metodnamn att försöka två gånger. Först försöker den hitta en EventHandler med det namnet, dvs. en metod med signaturen:

void EventHandler(object sender, EventArgs e)

Exempel: Metod som är en EventHandler

void Page_PreInit(object sender, EventArgs e)

Om det misslyckas kommer den försöka att hitta en VoidMethod, dvs. en metod med signaturen:

void VoidMethod()

Exempel: Metod som är en VoidMethod

void Page_PreInit()

Så om du inte har angivet någon metod alls som matchar något av kriterierna ovan i t.ex. en sida så kommer den ändå att leta efter 32 metoder (och inte 28 som K Scott Allen skrev).


Resultatet av matchningen kommer att lagras i en cache, och kommer därför inte att anropas varje gång. Men det kommer att anropas åtminstone en gång. :)


Haka metoden på eventet


För alla metoder som matchar kriterierna kommer TemplateControl.HookUpAutomaticHandlers() att haka metoden på motsvarande event. Detta kommer dock inte att göras om metoden redan har hakats på eventet eftersom HookUpAutomaticHandlers letar igenom alla event-hanterare för eventet och om den hittar metoden där så struntar den i att lägga till metoden igen.


Men i exemplet ovan så blev ju Page_Load() påhakat två gånger på samma event?
Jepp, men det var för att när vi i Page_Init hakade på eventet så hade HookUpAutomaticHandlers redan gjort det. HookUpAutomaticHandlers anropas ju i OnInit som körs före vår Page_Init. Om vi istället hakar på vårt event före sidans OnInit och därmed före HookUpAutomaticHandlers anropats så kommer Page_Load bara att anropas en gång.


Exempel: Page_Load anropas bara en gång.

public partial class _Default : System.Web.UI.Page
{
public _Default()
{
Page.Load += Page_Load;
}

protected void Page_Load(object sender, EventArgs e)
{
//This code will execute once
}
}

Slutsats: AutoEventWireup=false


Överväg att slå om AutoEventWireup till false (den är true per default) och haka själv på de event du vill använda. Du får mer kontroll över när saker anropas och du slipper den overhead som TemplateControl.HookUpAutomaticHandlers medför.


Microsoft själva rekommenderar att man slår av det när hög prestanda är viktigt.


Mer om AutoEventWireup på MSDN


http://msdn2.microsoft.com/en-us/library/system.web.configuration.pagessection.autoeventwireup.aspx
http://support.microsoft.com/default.aspx/kb/324151

2007-07-10

Vad Asp.Net-utvecklare bör kunna om JavaScript

K. Scott Allen har skrivit en utmärkt artikel om "What ASP.NET Developers Should Know About JavaScript"

Obligatorisk läsning om man ska koda lite mer avancerad JavaScript på klientsidan.

  • Varje objekt i JavaScript är ett dictionary
  • Vare funktion i JavaScript är även ett objekt
  • Varje objekt har en prototyp.

Förvirrad? Läs artikeln!

Utöka en befintlig AjaxControlToolkit Extender

I en tidigare bloggpost visade jag hur man skapar en AjaxControlToolkitExtender från början. Den här gången tänkte jag visa hur man utökar en befintlig extender med lite ytterligare funktionalitet. Det kan vara bra att läsa den tidigare bloggposten först.

HoverAddsCssClassExtender

Extendern HoverAddsCssClassExtender hakas liksom alla extendrar på en befinlig kontroll på sidan, t.ex. en Panel. När muspekaren förs över kontrollen kommer dess class-property (som innehåller css-klassnamn) att utökas med ytterligare ett css-klassnamn. När musen lämnar kontrollen återställs värdet. En klassisk onmouseover-, eller hover-effekt alltså. Traditionellt så brukar man byta css-klassnamnet mot något helt annat, men det är smartare att lägga på ytterligare en regel.

Ett litet exempel kan nog förtydliga vad som händer och hur den används:

<asp:Panel runat="server" ID="Panel1" CssClass="squared">
Hover me!
</asp:Panel>
<
ajaxExtender:HoverAddsCssClassExtender runat="server"
ID="HoverAddsCssClassExtender1" TargetControlID="Panel1" HoverCssClassName="hover" />

På klienten kommer Panel1 att vara en div vars class-property är satt till "squared". När musen förs över Panel1 kommer dess class-property att sättas till "squared hover", dvs. värdet från HoverCssClassName läggs till sist, för att återställas till "squared" när musen försvinner.


HoverExtender


Hur får man då till det här enklast? Jo det finns en extender som heter HoverExtender som följer med AjaxControlToolkit och som är lite odokumenterad. Den ligger som grund till DropDownExtender och HoverMenuExtender men den fungerar alldeles utmärkt att använda fristående. Det den gör att den kör ett av användaren specificerat script på mouseover och ett annat script på mouseout. Man kan även specificera att det ska dröja ett tag innan någon av scripten körs. Detta kan t.ex. utnyttjas till att tillåta att muspekaren passerar över kontrollen utan att scriptet körs (eller lämnar kontrollen som hastigast utan att scriptet körs direkt). Bara genom att låta muspekaren dröja kvar ett tag körs scriptet.


Vi kommer att låta vår extender ärva funktionalitet av HoverExtender. Men bara på klientsidan.


Skapa HoverAddsCssClassExtender


Nog pratat. Låt oss börja koda. Jag kommer inte att gå in i detalj hur du sätter upp projektet utan utgå ifrån att du har ett projekt där du kan skapa extendern. Mer information om hur du sätter upp ett sånt projekt finns i den tidigare bloggposten. Namnge projektet HoverAddsCssClass. 


Ny AjaxControlToolkitExtender


Du måste inte skapa Ajax Control Project utan kan skapa en extender i ett vanligt klassbibliotek. För att skapa en ny extender i ett befintligt projekt: Högerklicka på projektet och välj Add > New Item... och välj ASP.NET AJAX Extender Control längst ner under My Templates. Ange HoverAddsCssClass som namn. Se till att du har en referens till AjaxControlToolkit.


Nedan kommer jag förutsätta att du skapat ett nytt projekt som heter HoverAddsCssClass med tre filer. HoverAddsCssClassBehavior.js, HoverAddsCssClassDesigner.cs och HoverAddsCssClassExtender.cs. Det första man ALLTID ska göra är att se till att js-filen har Build Action=Embedded Resource (finns under properties för den filen).


Radera designern


Vi kommer inte att behöva någon egen designer så ta bort HoverAddsCssClassDesigner.cs ur projektet. Default-designern duger utmärkt. (Designern används i design-läget inne i Visual Studio). Gå samtidigt in och radera hänvisningen till den i HoverAddsCssClassExtender.cs.

namespace HoverAddsCssClass
{
[Designer(typeof(HoverAddsCssClassDesigner))]
[ClientScriptResource("HoverAddsCssClass.HoverAddsCssClassBehavior",
"HoverAddsCssClass.HoverAddsCssClassBehavior.js")]
[TargetControlType(typeof(Control))]
public class HoverAddsCssClassExtender : ExtenderControlBase
{
// TODO: Add your property accessors here.

Ärv av HoverExtender – men bara på klientsidan


Normalt ärver en extender (som används på server-sidan) från ExtenderControlBase som syns i koden ovan, men den kan bytas ut mot vilken extender som helst i AjaxControlToolkit (och alla andra klasser som implementerar IExtenderControl). Då vi vill återanvända funktionalitet som HoverExtender har skulle vi kunna byta ut arvet mot HoverExtender och få en massa saker gratis. Men eftersom vi inte vill exponera alla de properties som HoverExtender har så struntar vi i arvet. Istället lägger vi till de properties vi vill ha.


På klientsidan däremot vill vi utnyttja HoverBehavior (som är klientdelen av HoverExtender). Även här används arv. Längst ner i filen HoverAddsCssClassBehavior.js hittar du:

HoverAddsCssClass.HoverAddsCssClassBehavior.registerClass(
'HoverAddsCssClass.HoverAddsCssClassBehavior', AjaxControlToolkit.BehaviorBase);

Klassen HoverAddsCssClass.HoverAddsCssClassBehavior registreras och den ärver av AjaxControlToolkit.BehaviorBase, men det finns inget som säger att den måste ärva från den klassen. Vi ändrar det till att vara HoverBehavior istället

HoverAddsCssClass.HoverAddsCssClassBehavior.registerClass(
'HoverAddsCssClass.HoverAddsCssClassBehavior', AjaxControlToolkit.HoverBehavior);

Vi måste även se till att koden för HoverBehavior laddas ner till klienten. Det gör vi med hjälp av attributet RequiredScript på serversidans extender.

[ClientScriptResource("HoverAddsCssClass.HoverAddsCssClassBehavior", 
"HoverAddsCssClass.HoverAddsCssClassBehavior.js")]
[TargetControlType(typeof(Control))]
[RequiredScript(typeof(HoverExtender))]
public class HoverAddsCssClassExtender : Hover
{


Vi säger med RequiredScript-raden ovan att vi vill få ner alla script-filer som HoverExtender använder.


HoverCssClassName – Property


Vi behöver en property HoverCssClassName så att man kan ange css-klassnamnet som ska läggas till då musen förs över elementet. Vi börjar i extendern med att lägga till propertyn. Ta samtidigt bort MyProperty som redan ligger i filen.

[ExtenderControlProperty()]
[DefaultValue("")]
public string HoverCssClassName
{
get
{
return GetPropertyValue("HoverCssClassName", String.Empty);
}
set
{
SetPropertyValue("HoverCssClassName", value);
}
}


Över till klientsidans behavior-kod.

HoverAddsCssClass.HoverAddsCssClassBehavior = function(element) {
HoverAddsCssClass.HoverAddsCssClassBehavior.initializeBase(this, [element]);

this._hoverCssClassName = "";
}
...
dispose : function() {
// TODO: add your cleanup code here

HoverAddsCssClass.HoverAddsCssClassBehavior.
callBaseMethod(this, 'dispose');
},

get_HoverCssClassName : function() {
return this._hoverCssClassName;
},
set_HoverCssClassName : function(value) {
this._hoverCssClassName = value;
this.raisePropertyChanged('HoverCssClassName');
},


Hämta och spara värdet i en lokalt deklarerad variabel.


Hantera mouseover


HoverBehavior, klientsidans behavior som vi ärver av, har två properties hoverScript och unhoverScript, på vilka man anger javascriptkod i form av en strängar. Denna kod kommer att tolkas och köras vid mouseover resp. mouseout (och även focus resp. blur). Det här utnyttjar vi till att köra två av våra egna metoder för att lägga till och ta bort css-klassnamnet.


Eftersom vi ärver av HoverBehavior har vi tillgång till de metoder som används för att sätta dess properties. På samma sätt som vår HoverCssClassName har en metod set_HoverCssClassName för att ange värdet har hoverScript och unhoverScript de två metoderna set_hoverScript och set_unhoverScript. Så vi anropar dem från vår initialize-kod:

initialize : function() {
HoverAddsCssClass.HoverAddsCssClassBehavior.
callBaseMethod(this, 'initialize');

this.set_hoverScript("this._addCss()");
this.set_unhoverScript("this._removeCss()");
},

och vi lägger till de två metoder som initialize hänvisar till:

_addCss : function()
{
var targetElement=this.get_element();
if(targetElement && !Sys.UI.DomElement.containsCssClass(targetElement,this._hoverCssClassName))
{
Sys.UI.DomElement.addCssClass(targetElement,this._hoverCssClassName);
}
},

_removeCss : function()
{
var targetElement=this.get_element();
if(targetElement && Sys.UI.DomElement.containsCssClass(targetElement,this._hoverCssClassName))
{
Sys.UI.DomElement.removeCssClass(targetElement,this._hoverCssClassName);
}
}

I _addCss() börjar vi med att plocka ut det element som extendern hakades på. Om ett sånt element fanns (ska alltid finnas, men bäst att kolla ändå för att slippa fel om den ändå inte skulle finnas) och dess css-klassnamnsträng inte redan innehöll vår _hoverCssClassName lägg till det till css-klassnamnet.


_removeCss() fungerar på liknande sätt. Om vi har ett target element och dess css-klassnamnsträng innehåller _hoverCssClassName, ta bort det.


Vi nyttjar här tre funktioner som Asp.Net Ajax för med sig:



I och med det är allt klart.


Testsida


Läs den tidigare bloggposten för information om hur man skapar en testsajt. I Default.aspx knackar du in följande:

<head id="Head1" runat="server">
<title>Untitled Page</title>
<style type="text/css">
.squared
{
width:200px;
height:200px;
border: dashed 4px black;
background-color: #eee;
text-align:center;
}

.hover
{
background-color: #228;
color:white;
}
</style>

</head>
<
body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<asp:Panel runat="server" ID="Panel1" CssClass="squared">
Hover me!
</asp:Panel>
<myExtenders:HoverAddsCssClassExtender runat="server" ID="HoverAddsCssClassExtender1" TargetControlID="Panel1" HoverCssClassName="hover" />
</form>
</
body>
</
html>

Sen är det bara att trycka F5 och testa.


Utöka med HoverDelay & UnhoverDelay


HoverBehavior har ytterligare properties som vi enkelt kan utnyttja: hoverDelay och unhoverDelay. Genom att sätta dessa specificerar vi hur lång tid i millisekunder man måste befinna oss över elementet innan hoverScript körs, resp. hur länge vi måste befinna oss utanför för att unhoverScript ska köras. Att lägga till stöd för dessa två värden är enkelt. På klientsidan ärver vi ju av HoverBehavior så där är dessa properties redan exponerade. Det räcker således med att göra det i serversidans extender.

[ExtenderControlProperty]
[DefaultValue(0)]
[ClientPropertyName("hoverDelay")]
public int HoverDelay
{
get
{
return GetPropertyValue("HoverDelay", 0);
}
set
{
SetPropertyValue("HoverDelay", value);
}
}

[ExtenderControlProperty()]
[ClientPropertyName("unhoverDelay")]
[DefaultValue(0)]
public int UnhoverDelay
{
get
{
return GetPropertyValue("UnhoverDelay", 0);
}
set
{
SetPropertyValue("UnhoverDelay", value);
}
}

Composition istället för arv


I det här exemplet använde vi arv på klientsidan för att få den funktionalitet vi behövde. Ett annat sätt är använda composition, vilket innebär att att man istället för att ärva in önskat beteende skapar en privat HoverBehavior inne i vår behavior (och då ärver man från AjaxControlToolkit.BehaviorBase som vanligt). Man får då själv se till att förändringar av mina properties slår igenom även på HoverBehavior.


När och varför man ska välja arv eller composition ligger utanför denna genomgång och även hur man skapar med Composition. Är du nyfiken på hur man gör kolla in t.ex. HoverMenuBehavior i AjaxControlToolkit som använder composition för PopupBehavior och HoverBehavior.


Ladda ner projektet


Ladda ner projektet

2007-05-21

Infoga Do-nothing-kommentarer

Infoga alltid en //Do nothing kommentar på ställen där avsikten är att inget ska utföras.

Anta t.ex. att du har en konstructor i en Control som ärver av WebControl och det enda du vill göra är att ändra vilken tag som ska användas.
public MyControl()
:base(HtmlTextWriterTag.Div)
{
}
När någon annan senare läser koden (eller du själv en månad senare när du glömt allt) är risken stor att man undrar om koden är klar eller om det saknas något i konstruktorn eftersom den är tom. Genom att knacka in en kommentar tydliggör du för dig själv och andra att den verkligen ska vara tom.
public MyControl()
:base(HtmlTextWriterTag.Div)
{
//Do nothing
}

Jag har en code snippet för det här så jag behöver bara skriva don och trycka tab två gånger så infogas //Do nothing.

2007-05-20

Skapa en AjaxControlToolkit Extender

I den här genomgången ska vi skapa en AjaxControlToolkit Extender som man hakar på en TextBox. När användaren skriver in ett datum i något av de tillåtna formaten kommer datumet att formateras om till ett angivet format. Om användaren skriver in "070519" kommer det att formateras om till "2007-05-19". Detta sker på klienten utan något anrop till webbservern.


För att kunna göra detta måste du ha laddat ner och installerat ASP.NET 2.0 AJAX Extensions 1.0 och ASP.NET AJAX Control Toolkit från http://ajax.asp.net/downloads/.


Länk till projektet finns längst ner


Klientkoden – översikt


Microsoft har utökat Date-proptotypen med metoden Date.parseLocale(value, validFormats) som givet en sträng returnerar ett datum om värdet matchade något av de angivna formaten. De har också utökat med en format(displayFormat) metod för att formattera ett datum.


Med dessa två metoder har vi allt vi behöver. Genom att hantera change-eventet på en input-textbox får vi reda på när dess värde ändrats. När värdet ändrats skickas det till parseLocale som antingen returnerar null, eller ett datum. Om ett datum returneras så formatteras det med format och den resulterande strängen åker tillbaka till input-textboxen.


Nytt Ajax Control Project


Börja med att skapa ett nytt ASP.NET AJAX Control Project. (File > New > Project. Välj Visual C# och under My Templates välj ASP.NET AJAX Control Project). Ge projektet ett valfritt namn, t.ex. DateParser. Ett nytt projekt sätts upp med tre filer: DateParserBehavior.js, DateParserDesigner.cs och DateParserExtender.cs. (Dessa filer kan du även få i ett befintligt projekt genom att högerklicka på projektet och välja Add New Item och sen välja ASP.NET AJAX Control.)


En extender består av en server del, DateParserExtender.cs, och en klientdel, DateParserBehavior.js. Genom att sätta properties i server-kontrollen kan vi påverka hur klientdelens beteende ska vara. Den tredje filen är en designer-klass men den behöver man sällan in och peta i.


Det första man måste göra när man lagt till en ny extender är att verifiera att javascript-filen kommer att bäddas in som en resurs. Kolla att propertyn Build Action är satt till Embedded Resorce.
Javascript file, Build Action: Embedded Resource


Om du glömmer att bädda in den som en resurs kommer du att få ett meddelande om att resursen inte finns när du försökèr köra applikationen.



Serverkontrollen


DateParser Extendern (i DateParserExtender.cs) ska ha två properties: ValidFormats och DisplayFormat för att man ska kunna ange tillåtna format samt hur det formatterade datumet ska se ut. DateParserExtender.cs utgör själva server-kontrollen och den ärver av ExtenderControlBase från AjaxControlToolkit. Inledningsvis innehåller vår nya kontroll en property; MyProperty som fungerar som en mall för hur properties ska skrivas.

[ExtenderControlProperty]
[DefaultValue("")]
public string MyProperty
{
get
{
return GetPropertyValue("MyProperty", "");
}
set
{
SetPropertyValue("MyProperty", value);
}
}


Attributet ExtenderControlProperty signalerar att det här är en property vars värde ska ner till klienten. Om attributet saknas kommer värdet aldrig att föras över till Behaviorn (man kan alltså ha "vanliga" properties vars värden inte direkt påverkar klient-beteendet).



DefaultValue-attributet har att göra med hur koden för att skapa den här kontrollen ska se ut (och ligger utanför detta ämne) och ska sättas till propertyns defaultvärde.



För att hämta och sätta värden används metoderna GetPropertyValue resp. SetPropertyValue. I samband med att man hämtar värdet anger man det värde som ska gälla ifall propertyn inte har satts tidigare. Se till att det motsvarar DefaultValue-attributet.



Våra properties blir då alltså:

private const string _Default_ValidFormats = "yyMMdd;yyyyMM;ddyyyy-MM-dd;yy-MM-dd;yyyyMM-dd;yyyy-MMdd;yyMM-dd;yy-MMdd;dd MMM yyyy;MM/dd/yy;MM/dd/YYYY";
private const string _Default_DisplayFormat = "yyyy-MM-dd";

[ExtenderControlProperty]
[DefaultValue(_Default_DisplayFormat)]
public string DisplayFormat
{
get
{
return GetPropertyValue("DisplayFormat", _Default_DisplayFormat);
}
set
{
SetPropertyValue("DisplayFormat", value);
}
}

[ExtenderControlProperty]
[DefaultValue(_Default_ValidFormats)]
public string ValidFormats
{
get
{
return GetPropertyValue("ValidFormats", _Default_ValidFormats);
}
set
{
SetPropertyValue("ValidFormats", value);
}
}


Vi använder konstanter för defaultvärdena för att se till att attributet och GetPropertyValue använder samma värde (sen är det dessutom snyggare att göra så och vi gillar ju att skriva snygg kod). Då var serverdelen klar.



Klientens Behavior

En behavior kan ses som en klass. Den har en konstruktor, där dess privata variabler definieras; den har properties; publika och privata metoder samt en initialize och en dispose-metod. När en behavior skapas körs först konstruktorn, eventuell properties från servern sätts, varpå initialize körs. I initialize bygger man upp det som behaviorn behöver för att fungera, registrerar events, etc. När behaviorn ska tas bort körs dess dispose-metod. Här avregistrerar man sig från event och rensar upp efter sig. Observera att dispose kan komma att anropas flera gånger så det gäller att se till att koden klarar det.

Properties



Även filen DateParserBehavior.js innehåller lite exempelkod för att visa hur den ska kodas. En property på klientsidan består av tre delar: en privat variabel som skapas i konstruktorn högt upp i filen, en get-metod och en set-metod som man använder för att sätta värdet på propertyn. Så för propertyn MyProperty så skriver man inte obj.MyProperty="value"; istället gör man anropet obj.set_MyProperty("value");.



Så för våra properties behöver vi två variabler för att hålla deras värden. Vi anger också default-värden.

DateParser.DateParserBehavior = function(element) {

DateParser.DateParserBehavior.initializeBase(this, [element]);

// Property variables
this._displayFormat = "yyyy-MM-dd";
this._validFormats = "yyMMdd;yyyyMMdd;yyyy-MM-dd;yy-MM-dd;yyyyMM-dd;yyyy-MMdd;yyMM-dd;yy-MMdd;dd MMM yyyy;MM/dd/yy;MM/dd/YYYY";
}
Efter dispose-metoden lägger vi in get- och set-metoderna
dispose : function() {
// TODO: add your cleanup code here

DateParser.DateParserBehavior.callBaseMethod(this, 'dispose');
},


// Properties ------------------------------------------------
get_DisplayFormat : function() {
return this._displayFormat;
},
set_DisplayFormat : function(value) {
this._displayFormat = value;
},


get_ValidFormats : function() {
return this._validFormats;
},
set_ValidFormats : function(value) {
this._validFormats = value;
}
De privata variablerna (signaleras med att de börjar med ett understreck) kan namnges som man vill men namnen på get- och set-metoderna måste motsvara server-kontrollens properties. Eftersom vi har en ExtenderControlProperty-markerad property i server-kontrollen som heter ValidFormats måste vi även ha en get_ValidFormats och en set_ValidFormats metod på klienten.

ParseDate, sträng till datum



Innan vi skriver kod för eventhanteringen behöver vi en hjälpmetod för att anropa Date.parseLocale(value,formats). Se mitt tidigare inlägg om varför detta behövs.

_parseDate : function(value,formats)
{
//Create an array that initially will contain value.
//Add the elements from formats after the value.
var args=[value];
Array.addRange(args,formats);

//args contains now: [value, formats[0], formats[1], ... ]
//Call the parseLocale method using our args array as parameters
return Date.parseLocale.apply(Date,args);
}


Uppdatera inputboxen



Metoden som kommer att anropas när eventet change höjs:

_updateTargetElement : function()
{
var targetElement = this.get_element();
var value=targetElement.value;
if(value)
{
var parsedDate = this._parseDate(value,this._validFormatsArr);
if(parsedDate)
{
targetElement.value=parsedDate.format(this._displayFormat);
}
//Else, on parse error, do nothing.
}
}


Metoden börjar med att ta reda på vårt targetElement, dvs. vår inputbox, genom att anropa metoden get_element() (en metod vi ärvt från vår basklass). Om värdet har satts, försök att tolka det som ett datum. Om vi lyckades tolka det som ett datum, formattera det enligt specificerat format och uppdatera inputboxens value. Det enda som behöver en förklaring är den privata variabeln this._validFormatsArr.



Tillåtna format som array



Vi anger formatet som en semikolonseparerad sträng av godkända format, t.ex. "yyyyMMdd;yyMMdd". Strängen kommer vi att göra om till en array ["yyyyMMdd","yyMMdd"] och det är alltså det this._validFormatsArr innehåller. Vi deklarerar den i konstruktorn ihop med de andra.

    // Property variables
this._displayFormat = "yyyy-MM-dd";
this._validFormats = "yyMMdd;yyyyMMdd;yyyy-MM-dd;yy-MM-dd;yyyyMM-dd;yyyy-MMdd;yyMM-dd;yy-MMdd;dd MMM yyyy;MM/dd/yy;MM/dd/YYYY";

// Internal variables
this._validFormatsArr=null;


Arrayen fylls i Initialize-metoden.

initialize : function() {
DateParser.DateParserBehavior.callBaseMethod(this, 'initialize');

this._validFormatsArr=this._validFormats.split(";");
},


Event-hantering



Det sista som återstår är att registrera en eventhanterare för inputboxens change-event. Mönstret för event består typiskt av fyra delar (ibland vill man göra på andra sätt men det är överkurs):





  1. Skapa en funktion för eventhantering

  2. Deklarera en privat variabel i konstruktorn som kommer att innehålla en delegat (kan betraktas som en funktionspekare) för din eventhanterare.

  3. I initialize skapar man delegaten och registrerar den på eventet.

  4. I dispose avregistrerar man delegaten från eventet och sätter den privata variablen till null.


I vårt exempel blir det då så här.





  1. Skapa en funktion för eventhantering
    //Event handlers -----------------------------------------------
    _onValueChange : function(e)
    {
    this._updateTargetElement();
    }


  2. Deklarera en privat variabel i konstruktorn som kommer att innehålla en delegat (kan betraktas som en funktionspekare) för din eventhanterare.
    //Event handlers
    this._valueChangeHandler=null;


  3. I initialize skapar man delegaten och registrerar den på eventet.
    initialize : function() {
    DateParser.DateParserBehavior.callBaseMethod(this, 'initialize');

    var targetElement = this.get_element();

    this._validFormatsArr=this._validFormats.split(";");

    //Register Event handlers
    this._valueChangeHandler = Function.createDelegate(this, this._onValueChange);
    $addHandler(targetElement, 'change', this._valueChangeHandler);

    },


  4. I dispose avregistrerar man delegaten från eventet och sätter den privata variablen till null.
    dispose : function() {
    //Reminder: Might be called several times.

    var targetElement = this.get_element();

    if (this._valueChangeHandler)
    {
    $removeHandler(targetElement, 'change', this._valueChangeHandler);
    this._valueChangeHandler = null;
    }


    DateParser.DateParserBehavior.callBaseMethod(this, 'dispose');
    },


Sisådär då borde allt vara klart för en testtur.



Testprojekt



För att kunna testa behöver vi en testsajt. Lägg till en ny Ajax-webbsajt (File > Add > New Web Site ... > ASP.NET AJAX-enabled Web site). Lägg till en referens till vår DateParser (högerklicka på webbsajten i Solution Explorer och välj Add Reference > Projects > Date Parser > OK).



Öppna default.aspx och dra in en TextBox från toolboxen. Växla till source-läge och lägg till följande direktiv ovanför DOCTYPE-raden för att göra vår extender tillgänglig på sidan:

<%@ Register tagprefix="myExtenders" Namespace="DateParser" Assembly="DateParser" %>


Nedanför TextBox1 lägger du till:

<myExtenders:DateParserExtender ID="TextBox1Extender"
runat="server" TargetControlID="TextBox1" />


Sätt webbprojektet till att vara startprojekt och kör igång genom att trycka F5. Om allt fungerar som det ska, ska du kunna mata in 070519 i rutan, trycka tab och det formatteras om till 2007-05-19. Om du missat att säga till att JavaScript-filen ska bäddas in som en resurs är det nu du kommer att få ett felmeddelande om att resursen saknas i assembly:t.



Om du vill byta datumformat är det bara att göra det på DateParserExtendern:

<myExtenders:DateParserExtender ID="TextBox1Extender"
runat="server" TargetControlID="TextBox1"
DisplayFormat="MMM ddd dd yyyy" ValidFormats="yyMMdd" />


Dessa format kan du använda: http://msdn2.microsoft.com/en-us/library/bb79761a-ca08-44ee-b142-b06b3e2fc22b.aspx



För att slippa lägga till ett register-direktiv på alla sidor kan du lägga till följande i Web.Config.

<system.web>
<pages>
<controls>
<add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add tagPrefix="myExtenders" namespace="DateParser" assembly="DateParser"/>
</controls>
</pages>


En bra sak att alltid lägga in i sina behaviors är en metod för att trigga beteendet. Om beteendet är att poppa upp en ruta när något händer, lägg till en metod som poppar upp den. I vårt fall behöver vi en metod för att parse:a inputboxen och formattera om den, så vi lägger till följande publika metod:

//Public Methods -----------------------------------------------
updateTargetElement : function()
{
this._updateTargetElement();
}

Metoder som denna gör det möjligt att via kod trigga ett beteende och det är ibland väldigt praktiskt. Tyvärr är det något de flesta AjaxControlToolkit Extenders saknar.



Ladda ner projektet