So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.
So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.
First off, here are the relevant methods on Page for script stuff:
- GetPostBackEventReference
- RegisterArrayDeclaration
- RegisterClientScriptBlock
- RegisterStartupScript
- There are more, for more esoteric needs, but this is what i'm covering right now
GetPostBackEventReference
Like the docs hint at but don't quite say, this method returns the script that calls __doPostBack. However, never type the function name "__doPostBack". Neither function name nor the specifics of the args it takes, are guarenteed to be the same for future versions of asp.net. Just use the returned value of this function instead. Always.
The Three Registers
Here's the deal on how these three should work together. RegisterClientScriptBlock says what to do, RegisterArrayDeclaration says who to do it to, and RegisterStartupScript just says Go.
If that was confusing, here's a lot more detail...
RegisterClientScriptBlock is where you put generic, non-control-instance-referencing code. That's not to say it can't have knowledge of the control design, but it shouldn't have knowledge of any specific controls on the page, by ID or any other way. Any of your control's IDs should be variables that this library uses. This lets you have 1 big library of code that can just be plopped onto the page with no worries about multiple controls or page structure or anything. It should have an entry point function ( I recommend something like MetaBuilders_WebControls_FooControl_Init ) where everything gets set up, properties are set, event handlers are added, etc. I'll go into good practices for this script later, for now i'm going to stay at the overview level.
RegisterArrayDeclaration is where you put the control id's that you are avoiding in the code library. Simply pick a name for your array ( I recommend the namespace qualified plural name of your control, with _'s, MetaBuilders_WebControls_FooControls ) and use the ClientID or UniqueID as the value, depending on needs. ( needs which I'll go over later ) This array is then accessed in your library's Init function in order to get references to each control instance on the page. Sometimes you'll find a need to include more than just the ID in the array value, when that happens, you can use a special property-value syntax i'll get into later.
RegisterStartupScript should generally have one line of code. Just call the Init function and kick things off.
How To Write The Library
Ok, so far I've only said that the lib should have an Init, and use the ClientIDs in the registered array to do stuff. Here's how I would write the lib... First, a typical Init skeleton looks something like this:
function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }
// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var myFooControl = document.getElementById( MetaBuilders_FooControls[i] );
//TODO Do stuff with myFooControl
}
}
function MetaBuilders_FooControl_BrowserCapable() {
if ( typeof( document.getElementById ) == "undefined" ) {
return false;
}
//TODO Add any more checks you need to
return true;
}
If you need to support browsers that don't have getElementById, then you'll need to do a few emit the UniqueID into the array, and use a form/input searching function to find the correct control. That would look something like this:
function MetaBuilders_FooControl_FindControl(uniqueID) {
for( var i = 0; i < document.forms.length; i++ ) {
var theForm = document.forms[i];
var theControl = theForm[uniqueID];
if ( theControl != null ) {
return theControl;
}
}
return null;
}
But in short, the Init method loops thru the array of IDs, and does fun stuff with the control. But what if you have composite control, and you want to do fun stuff with the child controls. Then your array declaration and your Init function will look a bit different. You'll want to give RegisterArrayDeclaration the ClientIDs of all the child controls you want to interact with instead of just the one parent. So lets say you have a composite control with two textboxes inside. your array bit might look like this:
Page.RegisterArrayDeclaration("{ ID:'" + this.ClientID + "', firstTextBoxID:'" + this.firstTextBox.ClientID + "', secondTextBoxID:'" + this.secondTextBox.ClientID + "' }");
then your init function might change to this:
function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }
// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var fooControlProperties = MetaBuilders_FooControls[i];
var myFooControl = document.getElementById( fooControlProperties.ID );
var myFirstTextBox = document.getElementById( fooControlProperties.firstTextBoxID );
var mySecondTextBox = document.getElementById( fooControlProperties.secondTextBoxID );
//TODO Do stuff with myFooControl and the child controls
}
}
The syntax I used in the array declaration ends up looking like this:
var MetaBuilders_FooControls = new Array( { ID:'fooControl1', firstTextBoxID:'fooControl1_firstTextBox', secondTextBoxID:'fooControl1_secondTextBox } );
It declares each item in the array as an object with the properties you set with the propertyName:value syntax. This style can be used with any properties, not just child control ids.
This brings us to custom server property values that the script needs to use. If you have a property on your control that simply needs to be sent to the script for use there, then the easiest way is to add another item to the RegisterArrayDeclaration call and change Init to grab it from the array. If the property needs to be a two-way variable, get/set on both server and client, then I suggest you add a HtmlInputHidden control and use its .Value. The serverside property will simply wrap its value, and you can get-set it on the clientside easily once you emit its ClientID in the array.
Ok... now... clientside events. I suggest that you attach event handlers in the Init function instead of setting the onFoo attributes on the control. The reason for this is that it keeps all your script in one place. The serverside c# or VB code doesn't need to know the specifics of your javascript, it only needs to give the script the info it needs for the current instance via the array declaration. This help speed up dev time, as you can stay in one file for changing event handlers and such.
Ok, so what's the code look like then? Well, you basically have 2 choices, you can either set the .onfoo property, or use the addEventListener/attachEvent methods. Using the onfoo property method allows you to easily access the calling control of the event via the "this" keyword, which comes in quite handy. However, it has the problem that it completely takes over the event. If there's also a tag attribute for it, this will replace that one. The other method, of course, has the exact opposite characteristics. "this" can't refer to the control, but it doesn't interfere with anybody else.
So how do you decide which is best? My recommendation is to use the .onfoo property if you are attaching events to your own child controls, and use the addEventListener/attachEvent methods if you are attaching events to controls outside of your jurisdiction. So lets look at some code...
This example will show the .onfoo property way. this code goes inside the Init function:
myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;
mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;
....
function MetaBuilders_FooControl_UpdateOtherTextBox() {
// note that by using the method, "this" refers to the textbox for the event.
this.OtherTextBox.value = this.value;
}
Now, whenver one textbox changes, the other will be updated. Take a closer look at the event hookup. First I create a new method on each textbox, UpdateOtherTextBox, by setting this new property to a function pointer. then I set the onchange event to the textbox's own method. This two-line technique is what allows me to use "this" in the handler.
However, when you are attaching to random controls, you don't want to be just overtaking their events like this, so you have to do some gymnastics. Here's an example from my DefaultButtons control where I attach to the onfocus and onblur events of page-level textboxes:
if ( typeof( inputControl.addEventListener ) != "undefined" ) {
inputControl.addEventListener("focus",DefaultButton_RegisterDefault,false);
inputControl.addEventListener("blur",DefaultButton_UnRegisterDefault,false);
} else if ( typeof ( inputControl.attachEvent ) != "undefined" ) {
inputControl.attachEvent("onfocus",DefaultButton_RegisterDefault);
inputControl.attachEvent("onblur",DefaultButton_UnRegisterDefault);
} else {
inputControl.onfocus = DefaultButton_RegisterDefault;
inputControl.onblur = DefaultButton_UnRegisterDefault;
}
The reason that this code is so big is because browsers are very different. addEventListener is the official W3C way of attaching events. attachEvent is the IE way, and I default to the .onfoo way if neither is supported. The problem is that this complicates my handler code, as "this" no longer refers to the control raising the event. To fix this, you need to get a handler on the data for the event. Here's how you get the element raised by an event, again from my DefaultButtons code:
function DefaultButton_RegisterDefault(e) {
// src here is the control which raised the event.
var src = DefaultButton_GetSrcElement(e);
//Usefull stuff removed for clarity
}
function DefaultButton_GetSrcElement(e) {
if ( typeof( window.event ) != "undefined" ) {
return window.event.srcElement;
}
if ( e != null && typeof( e.target ) != "undefined" ) {
return e.target;
}
return null;
}
Ok, so once you have your custom properties and your event handlers set up, now you just implement whatever cool stuff you want to make your control do its thing.
Oh, one more thing with the code. If you take another look at this code:
myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;
mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;
You'll see that I actually make properties on each child control that reference the other child controls. This is a very useful thing to do, as it makes referencing the related child controls from the event handlers much easier.
and now we move on to...
Packaging
UPDATE: This part has changed, thanks to some great comments on the article.
So now that you know who to write the code, how to you incorporate it into your projects? I use vstudio for all my control development, so I'll tell you how I do it with that tool. What I've found to be the best is to simply put all the script code into a .js file as an embedded resource. Simply right-click on the project, and choose Add New Item, choose script file, and set it as an embedded resource in the properties window.
Now to emit your code to the browser you use code like this:
if ( !Page.IsClientScriptBlockRegistered(scriptKey) ) {
using (System.IO.StreamReader reader = new System.IO.StreamReader(typeof(FooControl).Assembly.GetManifestResourceStream(typeof(FooControl), "FooControl.js"))) {
String script = "<script language='javascript' type='text/javascript' >\r\n<!--\r\n" + reader.ReadToEnd() + "\r\n//-->\r\n</script>";
this.Page.RegisterClientScriptBlock(scriptKey, script);
}
}
Ok, that code is nice, but where do you put it?
This is generally how I handle the actual task of script registration:
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
this.RegisterScript();
}
protected virtual void RegisterScript() {
// All the code that calls Page.RegisterFoo methods goes here.
}
PreRender is generally a good place to put script registrations because it is the last event where you can call these methods and they still take effect. You want to do it as late as possible because you never know when a page developer is going to change properties on your control that might change the script you produce.
Another tip for the serverside code... You generally don't want to Register your script if your .Visible property is false. And, depending on your script and functionality, you might not want to Register your script if your .Enabled property is false.
So anyway, I hope you got some useful information here. If you have any questions, feel free to send me a message or leave a comment.
P.S. almost none of the code here has actually been tested. I just wrote this stuff directly into the blog. I don't think there are problems with the code itself, but hey, I may have missed something, and this was intended as conceptual code anyway. Let me know if you find something obviously wrong.
| using System; using System.IO; using System.Reflection; class App{ public static void Main(){ Assembly a = Assembly.GetExecutingAssembly(); Stream stream = a.GetManifestResourceStream("TextResource.txt"); StreamReader reader = new StreamReader(stream); Console.WriteLine(reader.ReadToEnd()); } } |
||
浙公网安备 33010602011771号