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.

Inga kommentarer: