So many things were improved in ASP.NET 2.0! Today I was reminded of some of these improvements and so I wanted to make a bit of a blog "Note to Self" about what they were.
Remember what it was like when you created custom server controls in ASP.NET 1.x? It was cool, and you could do it, but there were many things that caused pain. I created lots of custom controls in the 1.x days and packaged many of them for use by external users, so I got a pretty good feel for the "pain points". This weekend was the first time that I really sat down with the aim of creating a custom server control in ASP.NET 2.0 that I was hoping to package for external users and some of the changes surprised me.
Resources
The first notable improvement was in how resources are handled. In 1.x you would typically either add resources as embedded resources or, as I saw on many occassions - including with ASP.NET itself - package resources as external files to be copied to the filesystem. If you took the embedded resources path, here's the type of code that you had to write to extract your scripts and embed them in the page with your control:
Type t = typeof(timer) ;
Assembly ass = t.Assembly ;
using (StreamReader reader = new StreamReader(ass.GetManifestResourceStream(t, "TimerScript.js"))) {
string script = "<script language='javascript' type='text/javascript' >" + reader.ReadToEnd() + "</script>" ;
this.Page.RegisterClientScriptBlock(scriptKey, script);
}
There's some tricky, low-level code there - and it get's much more difficult if you want to maximise caching and stuff. In ASP.NET 2.0 there's a much more "typed" version of how to embed resources. Nikhil has a couple of great articles which describe how this works:
WebResourceAttribute
Web Resouces - Design Time
Essentially, to embed a JavaScript in a custom control in ASP.NET 2.0 you would do the following:
- Add your JavaScript file to your Custom Control library and set it's build action as "Embedded Resource"
- Add a WebResource assembly attribute to your assembly to mark your resource as web accessible:
[assembly: WebResource("Button.gif", ContentType.ImageGif)]
- If your resource is an image, you can now simply reference that image like so:
myButton.ImageUrl = Page.GetWebResourceUrl(typeof(MyCustomControl), "Button.gif");
Nikhil's articles explain all of the other details that you need to know about WebResourceAttribute and how it works.
Client Scripts
Even before tMicrosoft Ajax, ASP.NET 2.0 added new support for working with client scripts as a control author. Most importantly, they moved the script logic from the Page, ie:
Page.RegisterStartupScript(...) ;
to a new ClientScriptManager class:
Page.ScriptManager.RegisterStartupScript(...);
where ScriptManager represents an instance of the ClientScriptManager class. One of the interesting things about the ClientScriptManager class is that many of it's methods require a "Type" argument to be passed in. Here's an example of using the Type argument in the RegisterStartupScript method:
scripts.RegisterClientScriptResource(typeof(PageLoad), "MarkItUp.WebControls.PageLoadScripts.js");
Note that, for the first argument I pass in the type of my custom control class (PageLoad). This argument is used as a key for determining the scope of the resource that will be emitted. Here's a nice article by Rick Strahl which talks about the Type argument and discusses some gotchas with it:
http://west-wind.com/WebLog/posts/9837.aspx
Script Control
With the advent of Ajax, ASP.NET has included much more support for working with scripts and client-side object from within server-side code. I'd actually forgotten about some of this stuff until today when Glav tidied up some of my "old school" custom control code and replaced it with the shiny new stuff. Here's an overview of the changes that he made.
First, he changed my control so that, instead of inheriting from WebControl, it instead inherited from ScriptControl. This control implements the IScriptControl interface which means that it contains two important methods for control authors: GetScriptDescriptors, and GetScriptReferences.
GetScriptReferences
...allows you to return a collection of each script reference that you wish emit with your control. The syntax is really simple:
protected override IEnumerable<ScriptReference> GetScriptReferences() {
ScriptReference script = new ScriptReference("YourNamespace.YourJavascriptFile.js", "Your.Assembly");
yield return script;
}
GetScriptDescriptors
...is a very useful method which removes a lot of the fiddly and potentially buggy (and non-typed) code that control authors used to use to instantiate client side instances of their control and to set its properties. There are ScriptComponentDescriptors - for working with Ajax component client types - and ScriptBehaviourDescriptors - for working with Ajax behaviour client types. Under the hood, ScriptDescriptors map to the Ajax client $create method which is used to instantiate client types at runtime, but this provides a very type-safe and clean way of working with these client-side statements.
As a refresher, this is what the client-side $create Ajax method looks like:
$create(type, properties, events, references, element);
The ASP.NET syntax for creating Ajax types from the server is to use ScriptDescriptors. To create a type instance, simply create an instance of a descriptor and return it from the GetScriptDescriptors method of your ScriptControl:
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors() {
ScriptComponentDescriptor cmp = new ScriptComponentDescriptor("MyNamespace.MyClass");
yield return cmp;
}
To fill in other arguments in the $create statement, you can use the AddProperty, AddEvent, and AddComponentProperty methods to fill those in. In the example where you want to instantiate a client type named Samples.SimpleComponent and construct that type by setting it's AProp property to 5000, you can use the AddProperty method to fill in that constructor information like so:
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors() {
ScriptComponentDescriptor cmp = new ScriptComponentDescriptor("Samples.SimpleComponent");
cmp.AddProperty("AProp", 5000);
cmp.AddProperty("id", this.ID) ;
yield return cmp;
}
You can see what the Javascript code for the Samples.SimpleComponent looks like here:
http://ajax.asp.net/docs/tutorials/CreatingClientComponentPrototype.aspx